コード貢献の障壁: `test::nginx`
API7.ai
November 17, 2022
テストはソフトウェア開発において不可欠な部分です。テスト駆動開発(TDD)の概念は非常に人気があり、ほぼすべてのソフトウェア企業が品質保証(QA)チームを持ち、テスト作業を担当しています。
テストはOpenRestyの品質と高い評価の基盤ですが、同時にOpenRestyのオープンソースプロジェクトで最も軽視されている部分でもあります。多くの開発者が毎日lua-nginx-module
を使用し、時折フレームグラフを実行しますが、テストケースを実行する人はどれだけいるでしょうか?多くのOpenRestyベースのオープンソースプロジェクトでさえ、テストケースがありません。しかし、テストケースと継続的インテグレーションがないオープンソースプロジェクトは信頼できません。
しかし、商業企業とは異なり、ほとんどのオープンソースプロジェクトには専任のソフトウェアテストエンジニアがいません。では、彼らはどのようにコードの品質を保証しているのでしょうか?答えは簡単です。「テスト自動化」と「継続的インテグレーション」です。そのポイントは自動化と継続性であり、OpenRestyはこれらを最大限に実現しています。
OpenRestyには70のオープンソースプロジェクトがあり、それらのユニットテスト、統合テスト、パフォーマンステスト、モックテスト、ファジーテストなどの作業負荷は、コミュニティの貢献者が手動で解決するには非常に困難です。そのため、OpenRestyは初期から自動化テストに多くの投資を行いました。これは短期的にはプロジェクトの進行を遅らせるように見えるかもしれませんが、長期的にはこの分野への投資が非常にコストパフォーマンスが高いと言えます。そのため、他のエンジニアとOpenRestyのテストロジックとツールセットについて話すと、彼らは驚きます。
では、OpenRestyのテスト哲学について話しましょう。
コンセプト
test::nginx
はOpenRestyのテストアーキテクチャの核心であり、OpenResty自体と周辺のlua-resty
ライブラリがテストセットを組織化し、記述するために使用されています。これは非常に高いハードルを持つテストフレームワークです。その理由は、一般的なテストフレームワークとは異なり、test::nginx
はアサーションに基づいておらず、Lua言語を使用していないため、開発者はtest::nginx
を一から学び、テストフレームワークに関する既存の知識を逆転させる必要があるからです。
私はいくつかのOpenRestyの貢献者を知っていますが、彼らはOpenRestyにCやLuaのコードを提出できますが、test::nginx
を使用してテストケースを書くのは難しいと感じています。彼らはテストケースの書き方を知らないか、テストが失敗したときに修正方法がわからないのです。そのため、私はtest::nginx
をコード貢献の障壁と呼んでいます。
test::nginx
はPerl、データ駆動、DSL(ドメイン固有言語)を組み合わせています。同じテストケースセットに対して、パラメータと環境変数を制御することで、ランダム実行、複数回の繰り返し、メモリリーク検出、ストレステストなどの異なる効果を達成できます。
インストールと例
test::nginx
を使用する前に、そのインストール方法を学びましょう。
OpenRestyシステムにおけるソフトウェアのインストールに関しては、公式のCIインストール方法が最もタイムリーで効果的です。他のインストール方法では常にさまざまな問題が発生します。そのため、公式の方法を参考にすることをお勧めします。そこではtest::nginx
のインストールと使用方法も見つけることができます。手順は4つです。
- まず、Perlのパッケージマネージャー
cpanminus
をインストールします。 - 次に、
cpanm
を使用してtest::nginx
をインストールします。
sudo cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1)
- その後、最新のソースコードをクローンします。
git clone https://github.com/openresty/test-nginx.git
- 最後に、Perlの
prove
コマンドを使用してtest-nginx
ライブラリをロードし、/t
ディレクトリ内のテストケースセットを実行します。
prove -Itest-nginx/lib -r t
インストール後、test::nginx
の最も簡単なテストケースを見てみましょう。以下のコードは公式ドキュメントから適応されており、すべてのカスタマイズされた制御パラメータを削除しています。
use Test::Nginx::Socket 'no_plan';
run_tests();
__DATA__
=== TEST 1: set Server
--- config
location /foo {
echo hi;
more_set_headers 'Server: Foo';
}
--- request
GET /foo
--- response_headers
Server: Foo
--- response_body
hi
test::nginx
はPerlで書かれており、そのモジュールの1つとして機能しますが、上記のテストからPerlや他の言語の何かが見えますか?その通りです。test::nginx
は、NGINXとOpenRestyのテストのために特別に抽象化された、作者自身のPerl実装のDSLだからです。
そのため、この種のテストを初めて見たとき、私たちはおそらく理解できないでしょう。しかし、心配しないでください。上記のテストケースを分析してみましょう。
まず、use Test::Nginx::Socket;
は、Perlがライブラリを参照する方法であり、Luaのrequire
と同様です。これもまた、test::nginx
がPerlプログラムであることを思い出させます。
2行目のrun_tests();
は、test::nginx
のPerl関数であり、テストフレームワークのエントリ関数です。test::nginx
内の他のPerl関数を呼び出す場合、それらはrun_tests
の前に配置する必要があります。
3行目の__DATA__
は、その下にあるすべてがテストデータであることを示すフラグであり、Perl関数はこのフラグの前に完了する必要があります。
次の=== TEST 1: set Server
は、テストケースのタイトルであり、このテストの目的を示しています。また、内部で自動的に番号を割り当てるツールがあります。
--- config
はNGINXの設定フィールドです。上記のケースでは、LuaではなくNGINXコマンドを使用しています。Luaコードを追加したい場合は、content_by_lua
のようなディレクティブを使用します。
--- request
は、端末をシミュレートしてリクエストを送信するために使用されます。その後にGET /foo
があり、リクエストのメソッドとURIを指定します。
--- response_headers
は、レスポンスヘッダーを検出するために使用されます。その後のServer: Foo
は、レスポンスヘッダーに必ず現れる必要があるheader
とvalue
を示しています。そうでない場合、テストは失敗します。
最後の--- response_body
は、対応するボディを検出するために使用されます。その後のhi
は、レスポンスボディに必ず現れる必要がある文字列です。そうでない場合、テストは失敗します。
さて、これで最も簡単なテストケースの分析が終わりました。したがって、テストケースを理解することは、OpenResty関連の開発作業を完了するための前提条件です。
テストケースを書く
次に、実際のテストに取り掛かりましょう。前回の記事でMemcachedサーバーをどのようにテストしたか覚えていますか?そうです。resty
を使用して手動でリクエストを送信しました。それは以下のコードで表されます。
resty -e 'local memcached = require "resty.memcached"
local memc, err = memcached:new()
memc:set_timeout(1000) -- 1 sec
local ok, err = memc:connect("127.0.0.1", 11212)
local ok, err = memc:set("dog", 32)
if not ok then
ngx.say("failed to set dog: ", err)
return
end
local res, flags, err = memc:get("dog")
ngx.say("dog: ", res)'
しかし、手動で送信するのはスマートではありませんよね?心配しないでください。test::nginx
を学んだ後、手動テストを自動化テストに変えることができます。例えば:
use Test::Nginx::Socket::Lua::Stream;
run_tests();
__DATA__
=== TEST 1: basic get and set
--- config
location /test {
content_by_lua_block {
local memcached = require "resty.memcached"
local memc, err = memcached:new()
if not memc then
ngx.say("failed to instantiate memc: ", err)
return
end
memc:set_timeout(1000) -- 1 sec
local ok, err = memc:connect("127.0.0.1", 11212)
local ok, err = memc:set("dog", 32)
if not ok then
ngx.say("failed to set dog: ", err)
return
end
local res, flags, err = memc:get("dog")
ngx.say("dog: ", res)
}
}
--- stream_config
lua_shared_dict memcached 100m;
--- stream_server_config
listen 11212;
content_by_lua_block {
local m = require("memcached-server")
m.go()
}
--- request
GET /test
--- response_body
dog: 32
--- no_error_log
[error]
このテストケースでは、--- stream_config
、--- stream_server_config
、--- no_error_log
を設定項目として追加しましたが、それらは本質的に同じです。つまり、
テストのデータとテスト自体を抽象化することで、可読性と拡張性を向上させています。
これがtest::nginx
が他のテストフレームワークと根本的に異なる点です。このDSLは両刃の剣であり、テストロジックを明確にし、容易に拡張できるようにします。しかし、新しい構文と設定を再学習する必要があるため、学習コストが高くなります。
まとめ
test::nginx
は強力ですが、多くの場合、あなたのシナリオに常に適しているとは限りません。なぜ蝶を車輪で潰す必要があるでしょうか?OpenRestyでは、アサーションベースのテストフレームワークbusted
を使用するオプションもあります。busted
はresty
と組み合わせてコマンドラインツールになり、多くのテストニーズを満たすことができます。
最後に、1つ質問を残します。このMemcached
のテストをローカルで実行できますか?新しいテストケースを追加できると素晴らしいです。