コード貢献の障壁: `test::nginx`

API7.ai

November 17, 2022

OpenResty (NGINX + Lua)

テストはソフトウェア開発において不可欠な部分です。テスト駆動開発(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つです。

  1. まず、Perlのパッケージマネージャーcpanminusをインストールします。
  2. 次に、cpanmを使用してtest::nginxをインストールします。
sudo cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1)
  1. その後、最新のソースコードをクローンします。
git clone https://github.com/openresty/test-nginx.git
  1. 最後に、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は、レスポンスヘッダーに必ず現れる必要があるheadervalueを示しています。そうでない場合、テストは失敗します。

最後の--- 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を使用するオプションもあります。bustedrestyと組み合わせてコマンドラインツールになり、多くのテストニーズを満たすことができます。

最後に、1つ質問を残します。このMemcachedのテストをローカルで実行できますか?新しいテストケースを追加できると素晴らしいです。