`test::nginx`のテスト方法:設定、リクエスト送信、およびレスポンス処理
API7.ai
November 18, 2022
前回の記事で、私たちはすでにtest::nginx
の最初の一瞥を得て、最もシンプルな例を実行しました。しかし、実際のオープンソースプロジェクトでは、test::nginx
で書かれたテストケースは、サンプルコードよりもはるかに複雑で習得が難しいものです。そうでなければ、それは障害とは呼ばれないでしょう。
この記事では、test::nginx
で頻繁に使用されるコマンドとテスト方法を紹介し、OpenRestyプロジェクトのテストケースセットの大部分を理解し、より現実的なテストケースを書く能力を身につけられるようにします。まだOpenRestyにコードを貢献していなくても、OpenRestyのテストフレームワークに慣れることは、仕事でテストケースを設計し書く上で大きなインスピレーションとなるでしょう。
test::nginx
のテストは、本質的に各テストケースの設定に基づいてnginx.conf
を生成し、NGINXプロセスを起動します。その後、指定されたリクエストボディとヘッダーでクライアントリクエストをシミュレートします。次に、テストケース内のLuaコードがリクエストを処理し、レスポンスを行います。この時、test::nginx
はレスポンスボディ、レスポンスヘッダー、エラーログなどの重要な情報を解析し、テスト設定と比較します。不一致がある場合、テストはエラーで失敗します。それ以外の場合は成功です。
test::nginx
は多くのDSL(ドメイン固有言語)プリミティブを提供しています。私はNGINXの設定、リクエストの送信、レスポンスの処理、ログのチェックに従って簡単に分類しました。この20%の機能で80%のアプリケーションシナリオをカバーできるので、しっかりと把握する必要があります。他のより高度なプリミティブと使用方法については、次の記事で紹介します。
NGINX設定
まず、NGINX設定を見てみましょう。test::nginx
の「config」キーワードを持つプリミティブは、NGINX設定に関連しています。例えば、config
、stream_config
、http_config
などです。
それらの機能は同じです:指定されたNGINX設定を異なるNGINXコンテキストに挿入します。これらの設定は、NGINXコマンドでも、content_by_lua_block
にカプセル化されたLuaコードでもかまいません。
ユニットテストを行う際、config
は最もよく使用されるプリミティブで、Luaライブラリをロードし、ホワイトボックステストのために関数を呼び出します。以下はテストコードのスニペットで、完全には実行できません。実際のオープンソースプロジェクトからのものなので、興味があればリンクをクリックして完全なテストを見るか、ローカルで実行してみてください。
=== TEST 1: sanity
--- config
location /t {
content_by_lua_block {
local plugin = require("apisix.plugins.key-auth")
local ok, err = plugin.check_schema({key = 'test-key'})
if not ok then
ngx.say(err)
end
ngx.say("done")
}
}
このテストケースの目的は、plugins.key-auth
コードファイル内のcheck_schema
関数が正しく動作するかどうかをテストすることです。location /t
内のcontent_by_lua_block
NGINXコマンドを使用して、テスト対象のモジュールを要求し、チェックする必要がある関数を直接呼び出します。
これはtest::nginx
でのホワイトボックステストの一般的な手段です。しかし、この設定だけではテストを完了できないので、次に進んでクライアントリクエストを送信する方法を見てみましょう。
リクエストの送信
クライアントがリクエストを送信するシミュレーションには多くの詳細が含まれるので、最もシンプルなものから始めましょう - 単一のリクエストを送信します。
request
上記のテストケースを続けると、ユニットテストコードを実行するためには、configで指定された/t
アドレスにHTTPリクエストを開始する必要があります。以下のテストコードのように:
--- request
GET /t
このコードは、リクエストプリミティブで/t
にGET
リクエストを送信します。ここでは、アクセスするIPアドレス、ドメイン名、ポート、またはHTTP 1.0
かHTTP 1.1
かを指定していません。これらの詳細はすべてtest::nginx
によって隠されているので、気にする必要はありません。これはDSLの利点の一つです - ビジネスロジックに集中し、すべての詳細に気を散らさずに済みます。
また、これにより部分的な柔軟性が提供されます。例えば、デフォルトはHTTP 1.1
のプロトコルですが、HTTP 1.0
をテストしたい場合は、個別に指定できます:
--- request
GET /t HTTP/1.0
GET
メソッドに加えて、POST
メソッドもサポートする必要があります。以下の例では、指定されたアドレスに文字列hello world
をPOST
できます。
--- request
POST /t
hello world
ここでも、test::nginx
はリクエストボディの長さを計算し、host
とconnection
リクエストヘッダーを自動的に追加して、これが正常なリクエストであることを保証します。
もちろん、コメントを追加して読みやすくすることもできます。#
で始まる行はコードコメントとして認識されます。
--- request
# post request
POST /t
hello world
リクエストは、より複雑で柔軟なモードもサポートしています。これはeval
をフィルターとして使用し、Perlコードを直接埋め込むものです。test::nginx
はPerlで書かれているため、現在のDSL言語がニーズを満たさない場合、eval
はPerlコードを直接実行する「究極の武器」です。
eval
の使用法については、ここでいくつかの簡単な例を見てみましょう。他のより複雑な例については、次の記事で続けます。
--- request eval
"POST /t
hello\x00\x01\x02
world\x03\x04\xff"
最初の例では、eval
を使用して非表示文字を指定しています。これはその用途の一つです。二重引用符の間の内容はPerl文字列として扱われ、その後request
に引数として渡されます。
以下はもっと興味深い例です:
--- request eval
"POST /t\n" . "a" x 1024
しかし、この例を理解するには、Perlの文字列について少し知る必要があります。ここで簡単に2点を説明します。
- Perlでは、文字列の連結にドットを使用します。これは
Lua
の2つのドットに似ていませんか? - 小文字の
x
は、文字の繰り返し回数を示します。例えば、上記の"a" x 1024
は、文字「a」を1024回繰り返すことを意味します。
したがって、2番目の例は、POST
メソッドで/t
アドレスに1024
文字のa
を含むリクエストを送信することを意味します。
pipelined_requests
単一のリクエストを送信する方法を理解した後、複数のリクエストを送信する方法を見てみましょう。test::nginx
では、pipelined_requests
プリミティブを使用して、同じkeep-alive
接続内で複数のリクエストを順番に送信できます:
--- pipelined_requests eval
["GET /hello", "GET /world", "GET /foo", "GET /bar"]
例えば、この例では、同じ接続内でこれらの4つのAPIに順番にアクセスします。これには2つの利点があります:
- 1つ目は、多くの繰り返しテストコードを排除し、4つのテストケースを1つに圧縮できることです。
- 2つ目で最も重要な理由は、パイプラインリクエストを使用して、複数回のアクセス時にコードロジックに例外が発生するかどうかを検出できることです。
あなたは疑問に思うかもしれません、複数のテストケースを順番に書けば、実行フェーズでコードも複数回実行されるので、上記の2番目の問題もカバーされるのではないかと。
これはtest::nginx
の実行モードに帰着します。それはあなたが思うのとは異なる動作をします。各テストケースの後、test::nginx
は現在のNGINXプロセスをシャットダウンし、メモリ内のすべてのデータが消えます。次のテストケースを実行する際に、nginx.conf
が再生成され、新しいNGINX Worker
が起動します。このメカニズムにより、テストケースが互いに影響を与えないことが保証されます。
したがって、複数のリクエストをテストしたい場合、pipelined_requests
プリミティブを使用する必要があります。それに基づいて、レートリミット、コンカレンシーリミット、および他の多くのシナリオをシミュレートし、より現実的で複雑なシナリオでシステムが正しく動作するかどうかをテストできます。これも次の記事に残しておきます。なぜなら、複数のコマンドとプリミティブが関わるからです。
repeat_each
複数のリクエストをテストするケースについて話しましたが、同じテストを複数回実行するにはどうすればよいでしょうか?
この問題に対して、test::nginx
はグローバル設定を提供しています:repeat_each
、これはPerl関数で、デフォルトはrepeat_each(1)
で、テストケースが1回だけ実行されることを示します。したがって、以前のテストケースでは、個別に設定する必要はありません。
当然、run_test()
関数の前に設定できます。例えば、引数を2
に変更します。
repeat_each(2);
run_tests();
すると、各テストケースが2回実行されます。以下同様です。
more_headers
リクエストボディについて話した後、リクエストヘッダーを見てみましょう。上記で述べたように、test::nginx
はデフォルトでhost
とconnection
ヘッダーを付けてリクエストを送信します。他のリクエストヘッダーはどうでしょうか?
more_headers
はまさにそのために設計されています。
--- more_headers
X-Foo: blah
これを使用して、さまざまなカスタムヘッダーを設定できます。複数のヘッダーを設定したい場合は、複数行を設定します:
--- more_headers
X-Foo: 3
User-Agent: openresty
レスポンスの処理
リクエストを送信した後、test::nginx
の最も重要な部分はレスポンスの処理です。ここで、レスポンスが期待通りかどうかを判断します。ここでは、レスポンスボディ、レスポンスヘッダー、レスポンスステータスコード、ログの4つの部分に分けて紹介します。
response_body
リクエストプリミティブの対応物はresponse_body
で、以下はそれらの2つの設定の使用例です:
=== TEST 1: sanity
--- config
location /t {
content_by_lua_block {
ngx.say("hello")
}
}
--- request
GET /t
--- response_body
hello
このテストケースは、レスポンスボディがhello
の場合に合格し、他の場合はエラーを報告します。しかし、長い返信ボディをテストするにはどうすればよいでしょうか?心配しないでください、test::nginx
はすでにそれを処理しています。正規表現でレスポンスボディを検出することをサポートしています。以下のように:
--- response_body_like
^he\w+$
これにより、レスポンスボディに対して非常に柔軟に対応できます。さらに、test::nginx
はunlike
操作もサポートしています:
--- response_body_unlike
^he\w+$
この時点で、レスポンスボディがhello
の場合、テストは合格しません。
同じように、単一のリクエストの検出を理解した後、複数のリクエストの検出を見てみましょう。以下はpipelined_requests
と一緒に使用する例です:
--- pipelined_requests eval
["GET /hello", "GET /world", "GET /foo", "GET /bar"]
--- response_body eval
["hello", "world", "oo", "bar"]
もちろん、ここで重要なのは、送信するリクエストの数だけ、対応するレスポンスが必要であることです。
response_headers
次に、レスポンスヘッダーについて話しましょう。レスポンスヘッダーはリクエストヘッダーと同様で、各行がヘッダーのキーと値に対応します。
--- response_headers
X-RateLimit-Limit: 2
X-RateLimit-Remaining: 1
レスポンスボディの検出と同様に、レスポンスヘッダーも正規表現とunlike
操作をサポートしています。例えば、response_headers_like
、raw_response_headers_like
、raw_response_headers_unlike
などです。
error_code
3番目はレスポンスコードです。レスポンスコードの検出は直接比較をサポートし、like
操作もサポートしています。以下の2つの例のように:
--- error_code: 302
--- error_code_like: ^(?:500)?$
複数のリクエストの場合、error_code
を複数回チェックする必要があります:
--- pipelined_requests eval
["GET /hello", "GET /hello", "GET /hello", "GET /hello"]
--- error_code eval
[200, 200, 503, 503]
error_log
最後のテスト項目はエラーログです。ほとんどのテストケースでは、エラーログは生成されません。no_error_log
を使用して検出できます:
--- no_error_log
[error]
上記の例では、NGINX error.log
に文字列[error]
が現れると、テストは失敗します。これは非常に一般的な機能で、すべての通常のテストにエラーログの検出を追加することをお勧めします。
--- error_log
hello world
上記の設定は、error.log
にhello world
が存在するかどうかを検出しています。もちろん、Perlコードを埋め込んだeval
を使用して正規表現検出を実装することもできます。以下のように:
--- error_log eval
qr/\[notice\] .*? \d+ hello world/
まとめ
今日は、test::nginx
でリクエストを送信し、レスポンスをテストする方法を学びました。リクエストボディ、ヘッダー、レスポンスステータスコード、エラーログを含みます。これらのプリミティブの組み合わせで、完全なテストケースセットを実装できます。
最後に、思考問題です:抽象的なDSLであるtest::nginx
の利点と欠点は何ですか?自由にコメントを残して私と議論してください。また、この記事を共有して一緒に考え、コミュニケーションすることも歓迎します。