`test::nginx`のテスト方法:設定、リクエスト送信、およびレスポンス処理

API7.ai

November 18, 2022

OpenResty (NGINX + Lua)

前回の記事で、私たちはすでに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設定に関連しています。例えば、configstream_confighttp_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

このコードは、リクエストプリミティブで/tGETリクエストを送信します。ここでは、アクセスするIPアドレス、ドメイン名、ポート、またはHTTP 1.0HTTP 1.1かを指定していません。これらの詳細はすべてtest::nginxによって隠されているので、気にする必要はありません。これはDSLの利点の一つです - ビジネスロジックに集中し、すべての詳細に気を散らさずに済みます。

また、これにより部分的な柔軟性が提供されます。例えば、デフォルトはHTTP 1.1のプロトコルですが、HTTP 1.0をテストしたい場合は、個別に指定できます:

--- request
GET /t  HTTP/1.0

GETメソッドに加えて、POSTメソッドもサポートする必要があります。以下の例では、指定されたアドレスに文字列hello worldPOSTできます。

--- request
POST /t  
hello world

ここでも、test::nginxはリクエストボディの長さを計算し、hostconnectionリクエストヘッダーを自動的に追加して、これが正常なリクエストであることを保証します。

もちろん、コメントを追加して読みやすくすることもできます。#で始まる行はコードコメントとして認識されます。

--- 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はデフォルトでhostconnectionヘッダーを付けてリクエストを送信します。他のリクエストヘッダーはどうでしょうか?

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::nginxunlike操作もサポートしています:

--- 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_likeraw_response_headers_likeraw_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.loghello worldが存在するかどうかを検出しています。もちろん、Perlコードを埋め込んだevalを使用して正規表現検出を実装することもできます。以下のように:

--- error_log eval
qr/\[notice\] .*?  \d+ hello world/

まとめ

今日は、test::nginxでリクエストを送信し、レスポンスをテストする方法を学びました。リクエストボディ、ヘッダー、レスポンスステータスコード、エラーログを含みます。これらのプリミティブの組み合わせで、完全なテストケースセットを実装できます。

最後に、思考問題です:抽象的なDSLであるtest::nginxの利点と欠点は何ですか?自由にコメントを残して私と議論してください。また、この記事を共有して一緒に考え、コミュニケーションすることも歓迎します。