EUnited Nations Council
テストの必要性
私たちが書いてきたソフトウェアは、時間の経過とともに徐々に大きくなり、やや複雑になってきました。そうなると、Erlang シェルを起動し、何かを入力し、結果を見て、コードを変更した後で動作することを確認するのが、非常に面倒になります。時間が経つにつれて、手作業でチェックリストに従って確認するよりも、事前に用意されたテストをすべて実行する方が、誰にとっても簡単になります。これらは通常、ソフトウェアでテストが必要になる理由として非常に妥当なものです。また、テスト駆動開発のファンであれば、テストが役に立つでしょう。
逆ポーランド記法計算機を作成した章を思い出してください。手動で書いたテストがいくつかありました。それらは単純に、Result = Expression という形式のパターンマッチのセットであり、何か問題が発生するとクラッシュし、それ以外の場合は成功しました。これは自分で書く単純なコードには有効ですが、より本格的なテストを行う場合は、フレームワークのような、より良いものが絶対に必要になります。
ユニットテストの場合、EUnit(この章で説明します)を使用する傾向があります。統合テストの場合、EUnit と Common Test の両方で対応できます。実際、Common Test はユニットテストからシステムテスト、さらには Erlang で書かれていない外部ソフトウェアのテストまで、すべてを実行できます。今は、EUnit がもたらす良い結果に対して、どれほどシンプルかという点を考慮して、EUnit を使用します。
EUnit、EUnit とは何?
EUnit は、最も単純な形式では、モジュール内の _test() で終わる関数を、ユニットテストであると仮定して、自動的に実行する方法です。上記の RPN 計算機を掘り起こすと、次のコードが見つかります。
rpn_test() ->
5 = rpn("2 3 +"),
87 = rpn("90 3 -"),
-4 = rpn("10 4 3 + 2 * -"),
-2.0 = rpn("10 4 3 + 2 * - 2 /"),
ok = try
rpn("90 34 12 33 55 66 + * - +")
catch
error:{badmatch,[_|_]} -> ok
end,
4037 = rpn("90 34 12 33 55 66 + * - + -"),
8.0 = rpn("2 3 ^"),
true = math:sqrt(2) == rpn("2 0.5 ^"),
true = math:log(2.7) == rpn("2.7 ln"),
true = math:log10(2.7) == rpn("2.7 log10"),
50 = rpn("10 10 10 20 sum"),
10.0 = rpn("10 10 10 20 sum 5 /"),
1000.0 = rpn("10 10 20 0.5 prod"),
ok.
これは、計算機が正常に動作することを確認するために書いたテスト関数でした。古いモジュールを見つけて、これを試してください。
1> c(calc).
{ok,calc}
2> eunit:test(calc).
Test passed.
ok
eunit:test(Module). を呼び出すだけで済みました!やったー、これで EUnit がわかりました!シャンパンを開けて、別の章に進みましょう!
明らかに、これだけしかできないテストフレームワークはあまり役に立たず、技術的なプログラマーの専門用語で言えば、「あまり良くない」と表現されるかもしれません。EUnit は、_test() で終わる関数の自動エクスポートと実行以上のことができます。まず、テストを別のモジュールに移動して、コードとそのテストが混ざり合わないようにすることができます。これは、プライベート関数をテストできなくなることを意味しますが、モジュールのインターフェース (エクスポートされた関数) に対してすべてのテストを開発すれば、コードをリファクタリングするときにテストを書き直す必要がないことも意味します。2 つのシンプルなモジュールで、テストとコードを分離してみましょう。
-module(ops). -export([add/2]). add(A,B) -> A + B.
-module(ops_tests).
-include_lib("eunit/include/eunit.hrl").
add_test() ->
4 = ops:add(2,2).
したがって、ops と ops_tests があり、2 番目は最初のものに関連するテストを含んでいます。EUnit が実行できることは次のとおりです。
3> c(ops).
{ok,ops}
4> c(ops_tests).
{ok,ops_tests}
5> eunit:test(ops).
Test passed.
ok
eunit:test(Mod) を呼び出すと、自動的に Mod_tests が検索され、その中のテストが実行されます。失敗がどのように見えるかを確認するために、テストを少し変更しましょう (3 = ops:add(2,2) にします)。
6> c(ops_tests).
{ok,ops_tests}
7> eunit:test(ops).
ops_tests: add_test (module 'ops_tests')...*failed*
::error:{badmatch,4}
in function ops_tests:add_test/0
=======================================================
Failed: 1. Skipped: 0. Passed: 0.
error
どのテストが失敗したか (ops_tests: add_test...) と、その理由 (::error:{badmatch,4}) がわかります。また、合格または失敗したテストの完全なレポートも取得できます。ただし、出力はかなりひどいです。少なくとも通常の Erlang クラッシュと同じくらいひどいです。行番号もなく、明確な説明 (4 は一体何と一致しなかったのか?) もありません。テストを実行するのに、テストについてあまり教えてくれないテストフレームワークによって、私たちは為す術がありません。
このため、EUnit は私たちを助けるためのいくつかのマクロを導入しています。それらのそれぞれは、よりクリーンなレポート(行番号を含む)と、より明確なセマンティクスを提供してくれます。それらは、何かがうまくいかないことを知ることと、なぜうまくいかないかを知ることの違いです。
?assert(Expression), ?assertNot(Expression)- ブール値をテストします。
true以外の値が?assertに入ると、エラーが表示されます。?assertNotも同様ですが、負の値の場合です。このマクロは、true = Xまたはfalse = Yとほぼ同等です。 ?assertEqual(A, B)- 2 つの式 A と B の間で厳密な比較 (
=:=と同等) を行います。異なる場合、失敗が発生します。これは、ほぼtrue = X =:= Yと同等です。R14B04 以降、マクロ?assertNotEqualを使用して、?assertEqualの反対を行うことができます。 ?assertMatch(Pattern, Expression)- これにより、変数バインドなしで、
Pattern = Expressionに似た形式でマッチングできます。これは、?assertMatch({X,X}, some_function())のようにして、2 つの要素が同一であるタプルを受け取ったことをアサートできることを意味します。さらに、後で?assertMatch(X,Y)を実行しても、X はバインドされません。 - これは、
Pattern = Expressionのように適切であるというよりも、(fun (Pattern) -> true; (_) -> erlang:error(nomatch) end)(Expression)に近いということです。パターンの先頭の変数は、複数のアサーション間で決してバインドされません。R14B04 では、マクロ?assertNotMatchが EUnit に追加されました。 ?assertError(Pattern, Expression)- Expression がエラーになるはずであることを EUnit に伝えます。例として、
?assertError(badarith, 1/0)は成功したテストになります。 ?assertThrow(Pattern, Expression)?assertErrorとまったく同じですが、erlang:error(Pattern)の代わりにthrow(Pattern)を使用します。?assertExit(Pattern, Expression)?assertErrorとまったく同じですが、erlang:error(Pattern)の代わりにexit(Pattern)(exit/2ではない) を使用します。?assertException(Class, Pattern, Expression)- 前の 3 つのマクロの一般的な形式。例として、
?assertException(error, Pattern, Expression)は?assertError(Pattern, Expression)と同じです。R14B04 以降では、テストに使用できるマクロ?assertNotException/3もあります。
これらのマクロを使用すると、モジュール内でより良いテストを作成できます。
-module(ops_tests).
-include_lib("eunit/include/eunit.hrl").
add_test() ->
4 = ops:add(2,2).
new_add_test() ->
?assertEqual(4, ops:add(2,2)),
?assertEqual(3, ops:add(1,2)),
?assert(is_number(ops:add(1,2))),
?assertEqual(3, ops:add(1,1)),
?assertError(badarith, 1/0).
そして、それらを実行します。
8> c(ops_tests).
./ops_tests.erl:12: Warning: this expression will fail with a 'badarith' exception
{ok,ops_tests}
9> eunit:test(ops).
ops_tests: new_add_test...*failed*
::error:{assertEqual_failed,[{module,ops_tests},
{line,11},
{expression,"ops : add ( 1 , 1 )"},
{expected,3},
{value,2}]}
in function ops_tests:'-new_add_test/0-fun-3-'/1
in call from ops_tests:new_add_test/0
=======================================================
Failed: 1. Skipped: 0. Passed: 1.
error
エラーレポートがどれほど改善されたかを見てください。ops_tests の 11 行目の assertEqual が失敗したことがわかります。ops:add(1,1) を呼び出したとき、値として 3 を受け取ると考えていましたが、代わりに 2 を受け取りました。もちろん、これらの値を Erlang 用語として読む必要がありますが、少なくともそれらは存在します。
ただし、これで困るのは、5 つのアサーションがあったにもかかわらず、1 つだけが失敗したにもかかわらず、テスト全体が失敗と見なされていることです。いくつかのアサーションが失敗したことを、後続の他のすべてのアサーションも失敗したかのように振る舞うことなく知ることができれば、より良いでしょう。私たちのテストは、学校で試験を受けているようなもので、間違いを犯すとすぐに、あなたは失敗し、学校から追い出されます。そして、あなたの犬が死んで、あなたは最悪の日を迎えるのです。
テストジェネレーター
この柔軟性に対する一般的なニーズのため、EUnit はテストジェネレーターと呼ばれるものをサポートしています。テストジェネレーターは、関数にラップされたアサーションの略記のようなものであり、後で賢い方法で実行できます。_test() で終わる関数に ?assertSomething 形式のマクロを使用する代わりに、_test_() で終わる関数と ?_assertSomething 形式のマクロを使用します。これらは小さな変更ですが、はるかに強力になります。次の 2 つのテストは同等になります。
function_test() -> ?assert(A == B). function_test_() -> ?_assert(A == B).
ここで、function_test_() はテストジェネレーター関数と呼ばれ、?_assert(A == B) はテストジェネレーターと呼ばれます。それは、秘密裏に、?_assert(A == B) の基になる実装が fun() -> ?assert(A == B) end であるため、そのように呼ばれます。つまり、テストを生成する関数です。
テストジェネレーターの利点は、通常のアサーションと比較して、それらが fun であるということです。これは、実行せずに操作できることを意味します。実際には、次の形式のテストセットを持つことができます。
my_test_() ->
[?_assert(A),
[?_assert(B),
?_assert(C),
[?_assert(D)]],
[[?_assert(E)]]].
テストセットは、テストジェネレーターの深くネストされたリストにすることができます。テストを返す関数を持つこともできます!次のものを ops_tests に追加してみましょう。
add_test_() ->
[test_them_types(),
test_them_values(),
?_assertError(badarith, 1/0)].
test_them_types() ->
?_assert(is_number(ops:add(1,2))).
test_them_values() ->
[?_assertEqual(4, ops:add(2,2)),
?_assertEqual(3, ops:add(1,2)),
?_assertEqual(3, ops:add(1,1))].
add_test_() のみが _test_() で終わるため、2 つの関数 test_them_Something() はテストと見なされません。実際、それらは add_test_() によってのみ呼び出されてテストを生成します。
1> c(ops_tests).
./ops_tests.erl:12: Warning: this expression will fail with a 'badarith' exception
./ops_tests.erl:17: Warning: this expression will fail with a 'badarith' exception
{ok,ops_tests}
2> eunit:test(ops).
ops_tests:25: test_them_values...*failed*
[...]
ops_tests: new_add_test...*failed*
[...]
=======================================================
Failed: 2. Skipped: 0. Passed: 5.
error
そのため、予期された失敗が発生し、2 つのテストから 7 つのテストにジャンプしたことがわかります。テストジェネレーターの魔法です。
スイートの一部、おそらく add_test_/0 だけをテストしたい場合はどうでしょうか?EUnit にはいくつかの隠し技があります。
3> eunit:test({generator, fun ops_tests:add_test_/0}).
ops_tests:25: test_them_values...*failed*
::error:{assertEqual_failed,[{module,ops_tests},
{line,25},
{expression,"ops : add ( 1 , 1 )"},
{expected,3},
{value,2}]}
in function ops_tests:'-test_them_values/0-fun-4-'/1
=======================================================
Failed: 1. Skipped: 0. Passed: 4.
error
これはテストジェネレーター関数でのみ機能することに注意してください。ここで {generator, Fun} として持っているものは、EUnit の用語で言うと、テスト表現です。他にもいくつかの表現があります。
{module, Mod}は Mod 内のすべてのテストを実行します。{dir, Path}は、Path 内にあるモジュールのすべてのテストを実行します。{file, Path}は、単一のコンパイル済みモジュールにあるすべてのテストを実行します。{generator, Fun}は、上記のように、単一のジェネレーター関数をテストとして実行します。{application, AppName}は、AppName の.appファイルで言及されているすべてのモジュールのすべてのテストを実行します。
これらのさまざまなテスト表現により、アプリケーション全体またはリリース全体のテストスイートを簡単に実行できます。
フィクスチャ
アサーションとテストジェネレーターを使用するだけでは、アプリケーション全体をテストするのは非常に困難です。これが、フィクスチャが追加された理由です。フィクスチャは、テストをアプリケーションレベルまで稼働させるための万能ソリューションではありませんが、テストの周りに特定の足場を構築できます。
問題の足場は、テストごとにセットアップ関数と破棄関数を定義できる一般的な構造です。これらの関数を使用すると、各テストが役立つために必要な状態と環境を構築できます。さらに、足場を使用すると、テストの実行方法を指定できます(ローカルで実行するか、別のプロセスで実行するかなど)。
利用できるフィクスチャにはいくつかの種類があり、それらにはバリエーションがあります。最初のタイプは、単にセットアップフィクスチャと呼ばれます。セットアップフィクスチャは、次の多くの形式のいずれかを取ります。
{setup, Setup, Instantiator}
{setup, Setup, Cleanup, Instantiator}
{setup, Where, Setup, Instantiator}
{setup, Where, Setup, Cleanup, Instantiator}
ああ!これを理解するには、EUnit の語彙が少し必要になるようです (EUnit のドキュメントを読む必要がある場合に役立ちます)。
- セットアップ
- 引数を取らない関数。各テストには、セットアップ関数によって返された値が渡されます。
- クリーンアップ
- セットアップ関数の結果を引数として取り、必要なものをクリーンアップする関数。OTP で
terminateがinitの反対を行う場合、クリーンアップ関数は EUnit のセットアップ関数の反対になります。 - インスタンシエーター
- セットアップ関数の結果を取り、テストセットを返す関数です(テストセットは、
?_Macroアサーションのネストされたリストである可能性があることを覚えておいてください)。 - Where
- テストの実行方法を指定します:
local、spawn、{spawn, node()}。
さて、実際にはどのようなものになるのでしょうか?さて、架空のプロセスレジストリが、同じプロセスを異なる名前で 2 回登録しようとすることを正しく処理することを確認するためのテストを想像してみましょう。
double_register_test_() ->
{setup,
fun start/0, % setup function
fun stop/1, % teardown function
fun two_names_one_pid/1}. % instantiator
start() ->
{ok, Pid} = registry:start_link(),
Pid.
stop(Pid) ->
registry:stop(Pid).
two_names_one_pid(Pid) ->
ok = registry:register(Pid, quite_a_unique_name, self()),
Res = registry:register(Pid, my_other_name_is_more_creative, self()),
[?_assertEqual({error, already_named}, Res)].
このフィクスチャは、最初に start/0 関数内でレジストリサーバーを起動します。次に、インスタンシエーター two_names_one_pid(ResultFromSetup) が呼び出されます。そのテストでは、現在のプロセスを 2 回登録しようとするだけです。
そこでインスタンシエータが処理を行います。2回目の登録の結果は変数Resに格納されます。そして関数は、単一のテスト(?_assertEqual({error, already_named}, Res))を含むテストセットを返します。このテストセットはEUnitによって実行されます。その後、ティアダウン関数stop/1が呼び出されます。セットアップ関数によって返されたpidを使用することで、事前に開始していたレジストリをシャットダウンすることができます。素晴らしい!
さらに良いことに、このフィクスチャ全体をテストセットの中に配置することができます。
some_test_() ->
[{setup, fun start/0, fun stop/1, fun some_instantiator1/1},
{setup, fun start/0, fun stop/1, fun some_instantiator2/1},
...
{setup, fun start/0, fun stop/1, fun some_instantiatorN/1}].
そして、これは機能します!ここで厄介なのは、特に常に同じである場合に、セットアップ関数とティアダウン関数を常に繰り返す必要があることです。そこで、2つ目のタイプのフィクスチャであるforeachフィクスチャが登場します。
{foreach, Where, Setup, Cleanup, [Instantiator]}
{foreach, Setup, Cleanup, [Instantiator]}
{foreach, Where, Setup, [Instantiator]}
{foreach, Setup, [Instantiator]}
foreachフィクスチャは、インスタンシエータのリストを取るという点で、セットアップフィクスチャと非常に似ています。以下は、foreachフィクスチャで記述されたsome_test_/0関数です。
some2_test_() ->
{foreach,
fun start/0,
fun stop/1,
[fun some_instantiator1/1,
fun some_instantiator2/1,
...
fun some_instantiatorN/1]}.
こちらの方が良いでしょう。foreachフィクスチャは、各インスタンシエータを受け取り、それぞれのインスタンシエータに対してセットアップ関数とティアダウン関数を実行します。
これで、1つのインスタンシエータのフィクスチャと、多数のインスタンシエータのフィクスチャ(それぞれにセットアップ関数とティアダウン関数の呼び出しがある)を持つ方法がわかりました。もし、1回のセットアップ関数の呼び出しと、多数のインスタンシエータに対する1回のティアダウン関数の呼び出しが必要な場合はどうすればよいでしょうか?
言い換えれば、多くのインスタンシエータがあるが、状態を設定するのは1回だけにしたい場合はどうすればよいでしょうか?これを簡単に行う方法はありませんが、以下に役立つ可能性のある小さなトリックを示します。
some_tricky_test_() ->
{setup,
fun start/0,
fun stop/1,
fun (SetupData) ->
[some_instantiator1(SetupData),
some_instantiator2(SetupData),
...
some_instantiatorN(SetupData)]
end}.
テストセットが深くネストされたリストにできるという事実を利用して、インスタンシエータのように動作する匿名関数で複数のインスタンシエータをラップします。
テストは、フィクスチャを使用する際の実行方法をより細かく制御することもできます。4つのオプションが利用可能です。
{spawn, TestSet}- メインテストプロセスとは別のプロセスでテストを実行します。テストプロセスは、すべての生成されたテストが終了するのを待ちます。
{timeout, Seconds, TestSet}- テストはSeconds秒間実行されます。終了までにSeconds秒より長くかかる場合、テストはそれ以上の処理なしに終了されます。
{inorder, TestSet}- これにより、EUnitは、テストセット内のテストを返された順序で厳密に実行するように指示されます。
{inparallel, Tests}- 可能な場合、テストは並行して実行されます。
例として、some_tricky_test_/0テストジェネレータは、次のように書き換えることができます。
some_tricky2_test_() ->
{setup,
fun start/0,
fun stop/1,
fun(SetupData) ->
{inparallel,
[some_instantiator1(SetupData),
some_instantiator2(SetupData),
...
some_instantiatorN(SetupData)]}
end}.
これはフィクスチャに関するほとんどのことですが、もう一つ、今すぐには表示するのを忘れていた優れたトリックがあります。テストの説明を整然とした方法で記述できます。これを見てください。
double_register_test_() ->
{"Verifies that the registry doesn't allow a single process to "
"be registered under two names. We assume that each pid has the "
"exclusive right to only one name",
{setup,
fun start/0,
fun stop/1,
fun two_names_one_pid/1}}.
いいですね?読みやすいテストを得るために、{Comment, Fixture}を実行してフィクスチャをラップできます。これを実践してみましょう。
Regisのテスト
上記のような偽のテストを見るだけではあまり面白くないし、存在しないソフトウェアをテストするふりをすることはさらに悪いので、代わりにProcess Questで使用されているregis-1.0.0プロセスレジストリ用に書いたテストを見ていきます。
さて、regisの開発はテスト駆動型で行われました。うまくいけば、TDD(テスト駆動開発)を嫌っていないでしょうが、そうであったとしても、テストスイートを後から見るので、それほど悪いことにはならないはずです。これを行うことで、最初の書き込みで私が経験した可能性のあるいくつかの試行錯誤のシーケンスとバックペダリングを切り抜け、テキスト編集の魔法のおかげで、私は本当に有能であるように見えるでしょう。
regisアプリケーションは、スーパーバイザ、メインサーバー、およびアプリケーションコールバックモジュールの3つのプロセスで構成されています。スーパーバイザはサーバーのみをチェックし、アプリケーションコールバックモジュールは他の2つのモジュールのインターフェースとして動作する以外は何もしないことを知っているので、外部依存関係なしに、サーバー自体に焦点を当てたテストスイートを安全に作成できます。
優れたTDDファンとして、私はカバーしたいすべての機能のリストを書くことから始めました。
- Erlangのデフォルトのプロセスレジストリと同様のインターフェイスを尊重する
- サーバーには登録された名前があり、そのpidを追跡することなく連絡できるようにする
- プロセスはサービスを通じて登録でき、その後、その名前で連絡できるようになる
- 登録されているすべてのプロセスのリストを取得できる
- プロセスによって登録されていない名前は、それらを使用する呼び出しをクラッシュさせるために、アトム「undefined」を返す必要がある(通常のErlangレジストリと同様)
- プロセスは2つの名前を持つことができない
- 2つのプロセスが同じ名前を共有することはできない
- 登録されたプロセスは、呼び出し間で登録解除された場合、再度登録できる
- プロセスの登録解除は決してクラッシュしない
- 登録されたプロセスのクラッシュは、その名前を登録解除する
これは立派なリストです。要素を1つずつ実行し、ケースを適宜追加しながら、私は各仕様をテストに変換しました。最終的に得られたファイルは、regis_server_testsでした。私は次のような基本的な構造を使用して物事を書きました。
-module(regis_server_tests).
-include_lib("eunit/include/eunit.hrl").
%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% TESTS DESCRIPTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%
%%% SETUP FUNCTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%
%%% ACTUAL TESTS %%%
%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%
%%% HELPER FUNCTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%%
わかりました、モジュールが空のときは奇妙に見えますが、それを埋めていくにつれて、ますます意味を成してきます。
最初のテストを追加した後、最初のテストはサーバーを起動して名前でアクセスできる必要があるということで、ファイルは次のようになりました。
-module(regis_server_tests).
-include_lib("eunit/include/eunit.hrl").
%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% TESTS DESCRIPTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%
start_stop_test_() ->
{"The server can be started, stopped and has a registered name",
{setup,
fun start/0,
fun stop/1,
fun is_registered/1}}.
%%%%%%%%%%%%%%%%%%%%%%%
%%% SETUP FUNCTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%
start() ->
{ok, Pid} = regis_server:start_link(),
Pid.
stop(_) ->
regis_server:stop().
%%%%%%%%%%%%%%%%%%%%
%%% ACTUAL TESTS %%%
%%%%%%%%%%%%%%%%%%%%
is_registered(Pid) ->
[?_assert(erlang:is_process_alive(Pid)),
?_assertEqual(Pid, whereis(regis_server))].
%%%%%%%%%%%%%%%%%%%%%%%%
%%% HELPER FUNCTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%%
組織がわかりましたか?すでにかなり良くなりました。ファイルの上部には、フィクスチャと機能のトップレベルの説明のみが含まれています。2番目の部分には、必要になる可能性のあるセットアップ関数とクリーンアップ関数が含まれています。最後の部分には、テストセットを返すインスタンシエータが含まれています。
この場合、インスタンシエータは、regis_server:start_link()が実際に生きているプロセスを生成したか、およびregis_serverという名前で登録されたかを調べます。それが真実であれば、それはサーバーで機能します。
ファイルの現在のバージョンを見ると、最初の2つのセクションでは次のようになっています。
-module(regis_server_tests).
-include_lib("eunit/include/eunit.hrl").
-define(setup(F), {setup, fun start/0, fun stop/1, F}).
%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% TESTS DESCRIPTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%
start_stop_test_() ->
{"The server can be started, stopped and has a registered name",
?setup(fun is_registered/1)}.
register_test_() ->
[{"A process can be registered and contacted",
?setup(fun register_contact/1)},
{"A list of registered processes can be obtained",
?setup(fun registered_list/1)},
{"An undefined name should return 'undefined' to crash calls",
?setup(fun noregister/1)},
{"A process can not have two names",
?setup(fun two_names_one_pid/1)},
{"Two processes cannot share the same name",
?setup(fun two_pids_one_name/1)}].
unregister_test_() ->
[{"A process that was registered can be registered again iff it was "
"unregistered between both calls",
?setup(fun re_un_register/1)},
{"Unregistering never crashes",
?setup(fun unregister_nocrash/1)},
{"A crash unregisters a process",
?setup(fun crash_unregisters/1)}].
%%%%%%%%%%%%%%%%%%%%%%%
%%% SETUP FUNCTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%
start() ->
{ok, Pid} = regis_server:start_link(),
Pid.
stop(_) ->
regis_server:stop().
%%%%%%%%%%%%%%%%%%%%%%%%
%%% HELPER FUNCTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%%
%% nothing here yet
いいですね?スイートを書いているうちに、start/0とstop/1以外のセットアップ関数とティアダウン関数は必要ないことがわかりました。このため、?setup(Instantiator)マクロを追加しました。これにより、すべてのフィクスチャが完全に展開されるよりも、見栄えが少し良くなります。機能リストの各ポイントを多数のテストに変えたことは明らかです。サーバーの起動と停止(start_stop_test_/0)、プロセスの登録(register_test_/0)、プロセスの登録解除(unregister_test_/0)に関係するかどうかに応じて、すべてのテストを分割したことに注意してください。
テストジェネレータの定義を読むことで、モジュールが何をすることになっているかを知ることができます。テストはドキュメントになります(ただし、適切なドキュメントに代わるものではありません)。
テストを少し調べて、特定のやり方で物事がどのように行われたかを見ていきましょう。リストstart_stop_test_/0の最初のテストは、サーバーを登録できるという単純な要件があります。
start_stop_test_() ->
{"The server can be started, stopped and has a registered name",
?setup(fun is_registered/1)}.
テスト自体の実装は、is_registered/1関数にあります。
%%%%%%%%%%%%%%%%%%%%
%%% ACTUAL TESTS %%%
%%%%%%%%%%%%%%%%%%%%
is_registered(Pid) ->
[?_assert(erlang:is_process_alive(Pid)),
?_assertEqual(Pid, whereis(regis_server))].
以前のテストの最初のバージョンを見たときに説明したように、これはプロセスが利用可能かどうかをチェックします。erlang:is_process_alive(Pid)関数はあなたにとって新しいかもしれませんが、それ以外には特別なことはありません。その名前が示すように、プロセスが現在実行中かどうかをチェックします。サーバーを起動するとすぐにクラッシュする可能性がある、またはそもそも起動しない可能性があるという単純な理由で、そのテストを入れました。それは望ましくありません。
2番目のテストは、プロセスを登録できることに関連しています。
{"A process can be registered and contacted",
?setup(fun register_contact/1)}
テストは次のようになります。
register_contact(_) ->
Pid = spawn_link(fun() -> callback(regcontact) end),
timer:sleep(15),
Ref = make_ref(),
WherePid = regis_server:whereis(regcontact),
regis_server:whereis(regcontact) ! {self(), Ref, hi},
Rec = receive
{Ref, hi} -> true
after 2000 -> false
end,
[?_assertEqual(Pid, WherePid),
?_assert(Rec)].
確かに、これは最もエレガントなテストではありません。何をするかというと、自身を登録し、送信されたメッセージに返信するだけのプロセスを生成します。これはすべて、次のように定義されたcallback/1ヘルパー関数で行われます。
%%%%%%%%%%%%%%%%%%%%%%%%
%%% HELPER FUNCTIONS %%%
%%%%%%%%%%%%%%%%%%%%%%%%
callback(Name) ->
ok = regis_server:register(Name, self()),
receive
{From, Ref, Msg} -> From ! {Ref, Msg}
end.
したがって、関数はモジュール自体を登録し、メッセージを受信して応答を返します。プロセスが開始されると、register_contact/1インスタンシエータは15ミリ秒(他のプロセスが自身を登録するのを確実にするためのごくわずかな遅延)待機し、regis_serverのwhereis関数を使用してPidを取得し、プロセスにメッセージを送信しようとします。regisサーバーが正しく機能している場合、メッセージが返信され、関数の下部にあるテストでpidが一致します。
あまりにも多くのクールエイドを飲まないでください
そのテストを読むことで、私たちがしなければならなかった小さなタイマーの作業がわかりました。Erlangプログラムの並行性と時間に敏感な性質のために、テストは、コードのビットを同期しようとするという唯一の役割を持つ、小さなタイマーで頻繁に埋められます。
問題は、適切なタイマー、つまり十分に長い遅延と見なされるべきものを定義しようとすることになります。多数のテストを実行しているシステム、または負荷の高いコンピュータでは、タイマーはまだ十分に長く待機しているでしょうか?
テストを書くErlangプログラマーは、物事を機能させるために必要な同期の量を最小限に抑えるために、時には賢明である必要があります。これに対する簡単な解決策はありません。
次のテストは次のように紹介されています。
{"A list of registered processes can be obtained",
?setup(fun registered_list/1)}
したがって、多数のプロセスが登録されている場合、すべての名前のリストを取得できるはずです。これは、Erlangのregistered()関数呼び出しと同様の機能です。
registered_list(_) ->
L1 = regis_server:get_names(),
Pids = [spawn(fun() -> callback(N) end) || N <- lists:seq(1,15)],
timer:sleep(200),
L2 = regis_server:get_names(),
[exit(Pid, kill) || Pid <- Pids],
[?_assertEqual([], L1),
?_assertEqual(lists:sort(lists:seq(1,15)), lists:sort(L2))].
まず、登録されたプロセスの最初のリストが空であることを確認します(?_assertEqual(L1, []))。これにより、プロセスが自身を登録しようとしたことがない場合でも機能するものが得られます。次に、15個のプロセスが作成され、それらはすべて番号(1..15)で自身を登録しようとします。すべてのプロセスが自身を登録するのに十分な時間があることを確認するために、テストを少しスリープさせ、次にregis_server:get_names()を呼び出します。名前には、1から15までのすべての整数を含める必要があります。次に、登録されたすべてのプロセスを削除して、わずかなクリーンアップが行われます。結局のところ、それらをリークしたくありません。
テストがテストセットで使用する前に変数(L1とL2)に状態を格納する傾向があることに気付くでしょう。これは、返されるテストセットが、テストイニシエータ(コードのアクティブなビット全体)が実行された後で実行されるためです。他のプロセスや時間に敏感なイベントに依存する関数呼び出しを?_assert*マクロに入れようとすると、すべてが同期しなくなり、あなたとあなたのソフトウェアを使用する人にとって一般的にひどいことになります。
次のテストは簡単です。
{"An undefined name should return 'undefined' to crash calls",
?setup(fun noregister/1)}
...
noregister(_) ->
[?_assertError(badarg, regis_server:whereis(make_ref()) ! hi),
?_assertEqual(undefined, regis_server:whereis(make_ref()))].
ご覧のとおり、これは2つのことをテストします。undefinedを返すこと、およびundefinedを使用すると実際に呼び出しがクラッシュするという仕様の前提です。これについては、状態を保存するために一時変数を使用する必要はありません。レジスサーバーの状態をまったく変更しないと、両方のテストをレジスサーバーのライフサイクルの任意の時点で実行できます。
続けましょう。
{"A process can not have two names",
?setup(fun two_names_one_pid/1)},
...
two_names_one_pid(_) ->
ok = regis_server:register(make_ref(), self()),
Res = regis_server:register(make_ref(), self()),
[?_assertEqual({error, already_named}, Res)].
これは、前の章のデモで使用したテストとほぼ同じものです。ここでは、正しい出力が得られるかどうか、およびテストプロセスが異なる名前で2回登録できないかどうかを確認するだけです。
注意: 上記のテストでは、make_ref() を非常に多く使用していることに気づいたかもしれません。可能な限り、make_ref() のように一意の値を生成する関数を使用すると便利です。将来、誰かが並行してテストを実行したり、停止しない単一のレジストリサーバーでテストを実行したりする場合、テストを変更する必要なしにそうすることが可能になります。
すべてのテストで a、b、c のようなハードコードされた名前を使用した場合、多くのテストスイートを一度に実行しようとすると、遅かれ早かれ名前の衝突が発生する可能性が非常に高くなります。regis_server_tests スイートのすべてのテストがこのアドバイスに従っているわけではありません。ほとんどがデモンストレーション目的です。
次のテストは、two_names_one_pid の逆です。
{"Two processes cannot share the same name",
?setup(fun two_pids_one_name/1)}].
...
two_pids_one_name(_) ->
Pid = spawn(fun() -> callback(myname) end),
timer:sleep(15),
Res = regis_server:register(myname, self()),
exit(Pid, kill),
[?_assertEqual({error, name_taken}, Res)].
ここでは、2つのプロセスと、そのうち1つのプロセスからの結果のみが必要なため、トリックは、1つのプロセス(結果を必要としないプロセス)をスパンし、重要な部分を自分たちで行うことです。
タイマーを使用して、他のプロセスが最初に名前を登録しようとし(callback/1 関数内)、テストプロセス自体がエラータプル({error, name_taken})の結果を期待して、順番に試行するのを待つことを確認していることがわかります。
これにより、プロセスの登録に関連するテストのすべての機能がカバーされます。プロセスの登録解除に関連する機能のみが残っています。
unregister_test_() ->
[{"A process that was registered can be registered again iff it was "
"unregistered between both calls",
?setup(fun re_un_register/1)},
{"Unregistering never crashes",
?setup(fun unregister_nocrash/1)},
{"A crash unregisters a process",
?setup(fun crash_unregisters/1)}].
それらがどのように実装されるか見てみましょう。最初はややシンプルです。
re_un_register(_) ->
Ref = make_ref(),
L = [regis_server:register(Ref, self()),
regis_server:register(make_ref(), self()),
regis_server:unregister(Ref),
regis_server:register(make_ref(), self())],
[?_assertEqual([ok, {error, already_named}, ok, ok], L)].
リスト内のすべての呼び出しをシリアライズするこの方法は、すべてのイベントの結果をテストする必要があるときに私がよく使う巧妙なトリックです。それらをリストに入れることで、アクションのシーケンスを期待される [ok, {error, already_named}, ok, ok] と比較して、状況がどのように進んだかを確認できます。Erlang がリストを順番に評価する必要があることを指定するものは何もありませんが、上記のトリックはほぼ常に機能しています。
次のテスト、クラッシュしないことに関するテストは、次のようになります。
unregister_nocrash(_) ->
?_assertEqual(ok, regis_server:unregister(make_ref())).
ちょっと待ってください!それだけですか?はい、そうです。re_un_register を見返すと、プロセスの「登録解除」のテストをすでに処理していることがわかります。unregister_nocrash については、そこにないプロセスを削除しようとすることが機能するかどうかを知りたいだけです。
次に最後のテスト、そしてこれまでに行ったすべてのテストレジストリの中で最も重要なテストの1つは、クラッシュした名前付きプロセスは名前が登録解除されるということです。これは、名前を削除しなかった場合、名前の選択肢がどんどん狭まりながら、レジストリサーバーがどんどん大きくなるため、重大な意味を持ちます。
crash_unregisters(_) ->
Ref = make_ref(),
Pid = spawn(fun() -> callback(Ref) end),
timer:sleep(150),
Pid = regis_server:whereis(Ref),
exit(Pid, kill),
timer:sleep(95),
regis_server:register(Ref, self()),
S = regis_server:whereis(Ref),
Self = self(),
?_assertEqual(Self, S).
これは順番に読みます。
- プロセスを登録する。
- プロセスが登録されていることを確認します。
- そのプロセスを停止させます。
- プロセスのIDを盗む(真のスパイの方法)。
- 自分で名前を保持しているかどうかを確認します。
正直なところ、テストはより簡単な方法で書くことができました。
crash_unregisters(_) ->
Ref = make_ref(),
Pid = spawn(fun() -> callback(Ref) end),
timer:sleep(150),
Pid = regis_server:whereis(Ref),
exit(Pid, kill),
?_assertEqual(undefined, regis_server:whereis(Ref)).
死んだプロセスのIDを盗むという部分は、単なるつまらない泥棒の妄想に過ぎませんでした。
以上です!正しく行っていれば、コードをコンパイルしてテストスイートを実行できるはずです。
$ erl -make
Recompile: src/regis_sup
...
$ erl -pa ebin/
1> eunit:test(regis_server).
All 13 tests passed.
ok
2> eunit:test(regis_server, [verbose]).
======================== EUnit ========================
module 'regis_server'
module 'regis_server_tests'
The server can be started, stopped and has a registered name
regis_server_tests:49: is_registered...ok
regis_server_tests:50: is_registered...ok
[done in 0.006 s]
...
[done in 0.520 s]
=======================================================
All 13 tests passed.
ok
そうそう、「verbose」オプションを追加すると、テストの説明と実行時情報がレポートに追加されることに気づきましたか?それは素晴らしいです。
EUnitを編む者
この章では、EUnit のほとんどの機能の使用方法、それらで記述されたスイートの実行方法を見てきました。さらに重要なことに、現実世界で意味のあるパターンを使用して、並行プロセスに対するテストを作成する方法に関連するいくつかのテクニックを見てきました。
もう1つ知っておくべきトリックは、gen_server や gen_fsm などのプロセスをテストしたいときに、プロセスの内部状態を調べたいと感じるかもしれません。 sys モジュールによる素晴らしいトリックを次に示します。
3> regis_server:start_link().
{ok,<0.160.0>}
4> regis_server:register(shell, self()).
ok
5> sys:get_status(whereis(regis_server)).
{status,<0.160.0>,
{module,gen_server},
[[{'$ancestors',[<0.31.0>]},
{'$initial_call',{regis_server,init,1}}],
running,<0.31.0>,[],
[{header,"Status for generic server regis_server"},
{data,[{"Status",running},
{"Parent",<0.31.0>},
{"Logged events",[]}]},
{data,[{"State",
{state,{1,{<0.31.0>,{shell,#Ref<0.0.0.333>},nil,nil}},
{1,{shell,{<0.31.0>,#Ref<0.0.0.333>},nil,nil}}}}]}]]}
素晴らしいでしょう?サーバーの内部に関わるすべてのものが提供されます。必要なものはすべて、いつでも調べることができます!
サーバーなどのテストに慣れたい場合は、Process Quests のプレーヤーモジュール用に書かれたテストを読むことをお勧めします。それらは、handle_call、handle_cast、および handle_info へのすべての個々の呼び出しを独立して試す、別の手法を使用して gen_server をテストしています。それでもテスト駆動型で開発されましたが、そのニーズによって、物事を別に行う必要がありました。
いずれにせよ、Erlang のすべてのプロセスで利用可能なインメモリデータベースである ETS を使用するようにプロセスレジストリを書き直すときに、テストの真価がわかります。