`test::nginx` の知られざる使い方

API7.ai

November 24, 2022

OpenResty (NGINX + Lua)

前回の2つの記事で、test::nginxのほとんどの使い方をマスターし、OpenRestyプロジェクトのテストケースセットの大部分を理解できるようになったと思います。これは、OpenRestyとその周辺ライブラリを学ぶには十分です。

しかし、OpenRestyのコード貢献者になりたい場合や、プロジェクトでtest::nginxを使用してテストケースを書く場合、より高度で複雑な使い方を学ぶ必要があります。

今日の記事は、おそらくこのシリーズの中で最も「不人気」な部分になるでしょう。なぜなら、これまで誰も共有したことがない内容だからです。OpenRestyのコアモジュールであるlua-nginx-moduleを例にとると、世界中に70人以上の貢献者がいますが、すべての貢献者がテストケースを書いているわけではありません。ですから、今日の記事を読めば、test::nginxに対する理解が世界トップ100に入るでしょう。

テスト中のデバッグ

まず、開発者が通常のデバッグで使用する最もシンプルでよく使われるセクションを見ていきましょう。ここでは、これらのデバッグ関連のセクションの使用シナリオを一つずつ紹介します。

ONLY

多くの場合、元のテストケースセットに新しいテストケースを追加します。テストファイルに多くのテストケースが含まれている場合、実行に時間がかかります。特に、テストケースを繰り返し修正する必要がある場合です。

では、指定したテストケースのうち1つだけを実行する方法はあるでしょうか?これはONLYセクションで簡単に実現できます。

=== TEST 1: sanity
=== TEST 2: get
--- ONLY

上記の疑似コードは、このセクションの使い方を示しています。単独で実行したいテストケースの最後の行に--- ONLYを置くと、proveを使ってテストケースファイルを実行する際に、他のすべてのテストケースが無視され、このテストだけが実行されます。

ただし、これはデバッグ中にのみ適しています。そのため、proveコマンドがONLYセクションを見つけると、コードをコミットする際にそれを削除することを忘れないように促します。

SKIP

1つのテストケースだけを実行する要件に対応して、特定のテストケースを無視する要件もあります。SKIPセクションは、通常、まだ実装されていない機能をテストするために使用されます。

=== TEST 1: sanity
=== TEST 2: get
--- SKIP

この疑似コードからわかるように、その使い方はONLYと似ています。テスト駆動開発を行っているため、まずテストケースを書く必要があります。そして、実装を共同でコーディングする際に、実装の難易度や優先度によって機能の実装を遅らせる必要がある場合があります。その場合、対応するテストケースセットをスキップし、実装が完了したらSKIPセクションを削除します。

LAST

もう一つの一般的なセクションはLASTです。これも使い方は簡単で、それ以前のテストケースは実行され、それ以降のテストケースは無視されます。

=== TEST 1: sanity
=== TEST 2: get
--- LAST
=== TEST 3: set

ONLYSKIPの重要性は理解できますが、LASTの用途は何でしょうか?実際、テストケースに依存関係がある場合、後続のテストが意味を持つためには最初のいくつかのテストケースを実行する必要があります。そのため、このような場合、LASTはデバッグを続ける際に非常に役立ちます。

plan

test::nginxの機能の中で、planは最もイライラさせられ、理解しにくいものの一つです。これはPerlのTest::Planモジュールに由来しており、そのドキュメントはtest::nginxには含まれておらず、説明を見つけるのは容易ではありません。そのため、早い段階で紹介します。OpenRestyのコード貢献者の中には、この落とし穴にハマって抜け出せなかった人も何人かいます。

以下は、公式のOpenRestyテストセットの各ファイルの先頭で見られる類似の設定の例です。

plan tests => repeat_each() * (3 * blocks());

ここでのplanの意味は、テストファイル全体で計画されたテストの数です。最終的な実行結果が計画と一致しない場合、テストは失敗します。

この例では、repeat_eachの値が2で、テストケースが10個ある場合、planの値は2 x 3 x 10 = 60になります。唯一混乱する可能性があるのは、数字3の意味です。これはまるで魔法の数字のようです!

心配しないでください。例を見続ければ、すぐに理解できるでしょう。まず、以下のテストケースでplanの正しい値は何かわかりますか?

=== TEST 1: sanity
--- config
    location /t {
        content_by_lua_block {
            ngx.say("hello")
        }
    }
--- request
GET /t
--- response_body
hello

おそらく、plan = 1と結論づけるでしょう。なぜなら、テストはresponse_bodyのみをチェックするからです。

しかし、そうではありません!正しい答えはplan = 2です。なぜでしょうか?test::nginxには暗黙のチェック、つまり--- error_code: 200があり、デフォルトでHTTPレスポンスコードが200であるかどうかを検出します。

したがって、上記の魔法の数字3は、各テストが明示的に2回チェックされることを意味します。例えば、bodyerror logに対して、そして暗黙的にresponse codeに対してです。

これが非常に間違いやすいため、以下の方法でplanをオフにすることをお勧めします。

use Test::Nginx::Socket 'no_plan';

オフにできない場合、例えば公式のOpenRestyテストセットで不正確なplanに遭遇した場合、原因を深く追求せず、単にplanの式に数字を足したり引いたりすることをお勧めします。

plan tests => repeat_each() * (3 * blocks()) + 2;

これも公式に使用される方法です。

プリプロセッサ

同じテストファイルの異なるテストケース間で、いくつかの共通の設定がある場合があります。各テストケースで設定を繰り返すと、コードが冗長になり、後で修正するのが面倒になります。

このような場合、add_block_preprocessorディレクティブを使用して、Perlコードを追加できます。例えば以下のようになります。

add_block_preprocessor(sub {
    my $block = shift;

    if (!defined $block->config) {
        $block->set_value("config", <<'_END_');
    location = /t {
        echo $arg_a;
    }
    _END_
    }
});

このプリプロセッサは、すべてのテストケースにconfigセクションを追加し、内容はlocation /tです。これにより、後のテストケースでconfigを省略して直接アクセスできます。

=== TEST 1:
--- request
    GET /t?a=3
--- response_body
3

=== TEST 2:
--- request
    GET /t?a=blah
--- response_body
blah

カスタム関数

プリプロセッサにPerlコードを追加するだけでなく、run_tests関数の前に任意のPerl関数、つまりカスタム関数を追加することもできます。

以下は、ファイルを読み取り、evalディレクティブと組み合わせてPOSTファイルを実装する関数を追加する例です。

sub read_file {
    my $infile = shift;
    open my $in, $infile
        or die "cannot open $infile for reading: $!";
    my $content = do { local $/; <$in> };
    close $in;
    $content;
}

our $CONTENT = read_file("t/test.jpg");

run_tests;

__DATA__

=== TEST 1: sanity
--- request eval
"POST /\n$::CONTENT"

シャッフル

上記以外に、test::nginxにはあまり知られていない落とし穴があります。デフォルトでは、テストケースをランダムな順序で実行します。テストケースの順序と番号に従わないのです。

これは、より多くの問題をテストするために当初意図されていました。結局のところ、各テストケースが実行された後、NGINXプロセスが閉じられ、新しいNGINXプロセスが起動して実行されるため、結果は順序に関係ないはずです。

基盤レベルのプロジェクトでは、これは事実です。しかし、アプリケーションレベルのプロジェクトでは、データベースなどの永続的なストレージが外部に存在します。でたらめな実行は、誤った結果を招く可能性があります。毎回ランダムであるため、エラーが報告される場合とされない場合があり、エラーも毎回異なる可能性があります。これは明らかに開発者にとって混乱を招きます。私も何度もここでつまずきました。

したがって、私のアドバイスは次のとおりです。この機能をオフにしてください。以下の2行のコードでオフにできます。

no_shuffle();
run_tests;

特に、no_shuffleはランダム化を無効にし、テストケースの順序に厳密に従ってテストを実行します。

reindex

OpenRestyのテストケースセットには、厳格なフォーマット要件があります。各テストケースは3つの改行で区切る必要があり、テストケースの番号付けは厳密に自己増加する必要があります。

幸いなことに、この面倒な作業を行う自動ツールreindexがあります。これはopenresty-devel-utilsプロジェクトに隠されています。これに関するドキュメントはないため、ごく少数の人しか知りません。

興味があれば、テストケースの番号付けを混乱させたり、改行の数を増減させたりして、このツールを使用して整理し、元に戻せるかどうか試してみてください。

まとめ

これでtest::nginxの紹介は終わりです。もちろん、もっと多くの機能がありますが、核心的で最も重要なものだけを話しました。「魚を与えるのではなく、魚の釣り方を教えよ。」私はテストを学ぶための基本的な方法と注意点を教えました。その後、公式のテストケースセットを掘り下げて、より深く理解することができます。

最後に、以下の質問を考えてみてください。プロジェクト開発においてテストはありますか?そして、どのフレームワークを使用してテストしていますか?この記事を多くの人と共有して、交流し、学び合うことを歓迎します。