27. EUnited Nations Council

27.1. テストの必要性

../_images/eunit.png

私たちが書いてきたソフトウェアはだんだんと大きくなってきて、時が経つに連れていくらか複雑になってきました。 こうなってくると、コードが変更したあとにErlangシェルを起動して、何かを入力して、結果を見て、ちゃんと動いていることを確認する、というのがかなりつまらない作業になってきます。 時代の経過と共に、開発者がテストを行うのはより簡潔になってきて、いつも手動で確認事項のリストを一つ一つ追っていくようなものではなく、すべて事前に準備したテストを実行するだけになりました。 自分が書いたソフトウェアに対してテストをかけたい、というときは何かしら納得の行く理由があるものです。 あなたがテスト駆動開発のファンで、テストを便利なものだとも思っているということも有り得ますね。

逆ポーランド記法計算機 について書いた章を思い出すと、手動で行なってテストがいくつかありました。 単純に Result = Expression という形式のパターンマッチがひとまとまりあって、おかしな所があればクラッシュするし、そうでなければ成功するというものでした。 このようなテストは自分のために書いたちょっとしたコードであれば事足りるのですが、もっと真剣にテストをしたい場合、間違いなくもっと良い物、たとえばフレームワークのようなものが欲しくなるでしょう。

単体テストには、 EUnit (この章でご紹介します)を使い続けることが多いでしょう。 統合テストには、EUnitと Common Test のどちらも使えます。 事実、Common Testは単体テストからシステムテスト、果てはErlangで書かれていない外部ソフトウェアのテストまですべてこなすことができます。 まずは、EUnitについて学び、それが生み出す結果に対していかに簡潔であるか、ということも見ていきましょう。

27.2. EUnit、はてEUnit?とはなんぞや

EUnitとは、最も簡単に言えば、モジュール内の _test() で終わる関数を単体テストと想定して、それらを自動で走らせる方法の1つにすぎない、ということになるでしょう。 先に触れた逆ポーランド記法計算機を掘り下げていくと、次のようなコードを見つけられるでしょう:

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).

いま opsops_tests というモジュールがあります。2つ目のモジュールには1つ目に関するテストが書いてあります。 ここで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
../_images/lies.png

どのテストが失敗したか( ops_tests: add_test... )が確認でき、なぜ失敗したか( ::error:{badmatch,4} )が分かります。 また、いくつのテストが通って、いくつが失敗したかのレポートも取得できます。 しかし、出力結果はかなり雑です。 少なくとも、通常のErlangのクラッシュ時のレポートくらい悪いものです。行番号はないし、明確な説明もありません。( 4 が何と合わなかったのか、具体的にはなんでしょう) テストは実行するけれど、その実行結果について多くを話さないテストフレームワークでは、どうしようもありません。

このような理由から、EUnitでは助けとなるマクロがいくつかあります。 それぞれが、(行番号も含め)はっきりとしたレポートやセマンティクスを与えてくれます。 これらは何がうまく行かなかったか、ということと、どうしてうまく行かなかったか、の違いです:

?assert(Expression), ?assertNot(Expression)

これは真偽値に対するテストを行います。 true 以外の値が ?assert に入った場合はエラーが表示されます。 ?assertNot に関しても同様ですが、エラーになるのは false 以外の値が入った場合です。 このマクロは true = Xfalse = Y と同様のものです。

?assertEqual(A, B)

2つの式 AB に対して厳密な比較( =:= と等価)を行います。 それらが異なる場合、失敗となります。 これは大まかにいえば 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) に近い、ということです。 パターンの先頭にある変数は決して複数のアサーションのまたがって束縛されない、ということです。 ?assertNotMatch というマクロがR14B04のEUnitから追加されました。

?assertError(Pattern, Expression)

EUnitに、 Expression がエラーになるべきだということを教えます。 例として、 ?assertError(badarith, 1/0) はテストとして成功となります。

?assertThrow(Pattern, Expression)

?assertError とほぼ一緒ですが、 Expressionerlang:error(Pattern) ではなく throw(Pattern) になるかをテストします。

?assertExit(Pattern, Expression)

?assertError とほぼ一緒ですが、 Expressionerlang: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つ失敗しただけでテスト全体が失敗したとされる点です。 いくつかのアサーションが失敗しても、他のテストは失敗しなかった、ということを知ることができたほうが素敵でしょう。 私たちのテストは、学校で試験を受けるようなもので、間違ったらすぐに不合格となり、放校されます。 そして飼い犬は死んで、最悪な一日となるのです。

27.3. テストジェネレータ

このような柔軟性がしばしば求められるので、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. という実装を行なっているためです。 言い換えると、テストを生成する関数になっているのです。

通常のアサーションと比較した場合のテストジェネレータの利点は、これらが関数であることです。 つまり、実行しなくとも操作が可能だ、ということです。 事実、次のような形式で テストセット を作ることができます:

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_() で終わるので、 test_them_Something() の2つの関数はテストとみなされません。 事実、これらは 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} は1つのコンパイル済みモジュール内にあるすべてのテストを実行します
  • {generator, Fun} は、いま上記で取り上げたように、1つのジェネレータ関数をテストとして実行します
  • {application, AppName}AppName.app ファイル内で参照されているすべてのモジュール内のすべてのテストを実行します

これらのテスト表明を使い分けることで、アプリケーション全体のテスト、あるいはリリース全体でさえも簡単にテストスイートを走らせることができます。

../_images/fixture.png

27.4. フィクスチャー

とはいえ、アプリケーション全体をアサーションとテストジェネレータだけを使ってテストするのは非常に厳しいでしょう。 こういった理由から フィクスチャー が導入されました。 フィクスチャーは、アプリケーションレベルでテストを実行するための包括的な解決法ではなく、テストに関して特定の足場作りを可能にするものです。

いま欲しい足場は、各テストにsetupとteardownの関数を定義できる汎用的な構造をしたものです。 これらの関数によって、個々のテストを実行しやすいようにするために必要な状態と環境設定が可能になります。 さらに、この足場によってテストをどのように実行するかを支持することもできるのです。(テストをローカル環境で実施したいか、異なるプロセスで実行したいか、など)

何種類かのフィクスチャーがあり、さらにそれらにバリエーションがあります。 最初は単純に setup フィクスチャーと呼ばれるものです。 setupフィクスチャーは次のような多くの形式のどれかとなります。

{setup, Setup, Instantiator}
{setup, Setup, Cleanup, Instantiator}
{setup, Where, Setup, Instantiator}
{setup, Where, Setup, Cleanup, Instantiator}

しまった!これを理解するにはEUnit用語を先に覚えておく必要がありました(これらの用語はEUnitのドキュメントを読む必要が出てきた時に役に立ちます):

Setup:引数を取らない関数です。各テストにはsetup関数の戻り値が渡されます。
Cleanup:setup関数の結果を引数として受け取る関数で、必要なものの片付けを行います。 OTPで terminateinit の逆の処理を行うように、EUnitにおいてcleanup関数はsetup関数の逆の処理を行います。
Instantiator:この関数はsetup関数の結果を受け取り、テストセットを返します。(テストセットは ?_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) というInstantiatorが呼ばれます。 テストの中で行なっていることは、現在のプロセスを2度登録しようとすることだけです。

これがInstantiatorが仕事をしている部分です。 2回目の登録のの結果は Res という変数に保存されます。 その後関数は1つのテストを含んだテストセットを返します。( ?_assertEqual({error, already_named}, Res) ) そのテストがEUnitによって実行されます。 それからteardown関数の stop/1 が呼ばれます。 setup関数によって返された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}].

これはちゃんと動作します! ここで面倒なのは、setup関数とteardown関数を毎回繰り返す必要があるということで、特にそれらが常に同じ場合には面倒です。 ここで、2つ目のフィクスチャーである、 foreach フィクスチャーの登場です:

{foreach, Where, Setup, Cleanup, [Instantiator]}
{foreach, Setup, Cleanup, [Instantiator]}
{foreach, Where, Setup, [Instantiator]}
{foreach, Setup, [Instantiator]}

foreachフィクスチャーはsetupフィクスチャーに非常に似ていますが、Instantiatorのリストを受け取るところが違っています。 ここに、foreachフィクスチャーを使った some_test_/0 関数があります:

some_test2_() ->
    {foreach
     fun start/0,
     fun stop/1,
     [fun some_instantiator1/1,
      fun some_instantiator2/1,
      ...
      fun some_instantiatorN/1]}.

こちらのほうがいいですね。 foreachフィクスチャーは各Instantiatorを受け取って、setup関数とteardown関数をそれぞれに対して行います。

これで、1つのInstantiatorに対するフィクスチャーをどう使えば良いかがわかったので、今度は多くのInstantiatorに対してのフィクスチャーについて考えましょう。(各Instantiatorにそれぞれのsetupとteardown関数が呼ばれます) もしたくさんのInstantiatorに対してsetup関数とteardown関数を各々1度しか呼びたくない場合はどうしたら良いでしょうか。

言い換えると、たくさんのInstantiatorがあるけれど、ある状態を1度だけ設定したい場合はどうしたら良いでしょうか。 これを実現する簡単な方法はありませんが、ちょっとした技で実現できそうです:

some_tricky_test_() ->
    {setup,
     fun start/0,
     fun stop/1,
     fun (SetupData) ->
        [some_instantiator1(SetupData),
         some_instantiator2(SetupData),
         ...
         some_instantiatorN(SetupData)]
    end}.

テストセットが深い入れ子のリストであるという事を利用して、大量のInstantiatorを匿名関数でラップして、setup関数とteardown関数に対する1つのInstantiatorのように振舞わせます。

../_images/fatspawn.png

フィクスチャーをつかうと、テストがどのように動作すべきかに関してより細かい制御が出来るようにもなります。 4つのオプションが選べます:

{spawn, TestSet}:
 テストをメインのテストプロセスと異なるプロセス上で実行できます。 テストプロセスはすべての生成されたテストが終了するまで待ちます。
{timeout, Seconds, TestSet}:
 テストは Seconds 秒実行されます。 もし Seconds よりも長い時間かかる場合、それ以上面倒なことはすることなく終了されます。
{inorder, TestSet}:
 これはEUnitに、テストセット内のテストを返された順にきちんと実行されるように伝えます。
{inparallel, Tests}:
 可能であれば、テストは並列に実施されます。

例として、 some_tricky_test_/0 テストジェネレータは次のように書き直せます:

some_tricky_test2_() ->
    {setup,
     fun start/0,
     fun stop/1,
     fun(SetupData) ->
       {inparallel,
        [some_instantiator1(SetupData),
        some_instantiator2(SetupData),
        ...
        some_instantiatorN(SetupData)]}
     end}.

これでフィクスチャーに関してはほとんどおしまいですが、まだ1つだけみなさんにお見せしていないとっておきがあります。 テストの説明書きを見やすく書くことができます。 これを見て下さい:

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} のようにラップして、読みやすいテストにすることができます。 ぜひ実際に使ってみて下さい。

27.5. Regisをテストする

ここまでで見てきたような偽物のテストはあまりおもしろくなく、存在しないソフトウェアのテストをする振りというのはもっとよろしくないので、代わりにProcess Questで使われている、 regis-1.0.0 プロセスレジストリのために書いたテストについて見ていきましょう。

../_images/regis.png

いま、テスト駆動で regis の開発が終わりました。 幸いにもあなたはTDD(テスト駆動開発)が嫌いではありませんでしたが、たとえ嫌いだったとしても、別にそれはそれほど悪いことではありません。なぜなら、後でテストスイートを見ていくからです。 これをすることによって、いくつかの試行錯誤の繰り返しや、私が初めてこれを書いたときのような手戻りを手早く済ませることができ、文章の編集の魔力により私がとても有能に見えることでしょう。

regisアプリケーションは3つのプロセスから成り立っています。スーパバイザ、メインサーバ、アプリケーションコールバックモジュールの3つです。 スーパバイザはサーバのみを確認し、アプリケーションコールバックモジュールは他の2つのモジュールのインターフェースの役割しかしないことを理解した上で、サーバ自身に対して、他の外部依存なしに、テストスイートを安全に書くことができます。

私はかなりのTDDファンなので、確認しておきたいすべての機能のリストを書くことから始めます:

  • インターフェースをErlangのデフォルトのプロセスレジストリに似せる
  • Pidを追跡することなく連絡を取れるように、サーバは登録名を持つ
  • プロセスは私たちのサービス経由で登録され、その名前によって連絡を取ることができる
  • すべての登録済みプロセスのリストが取得できる
  • どのプロセスによっても登録されていない名前は、その名前を使った呼び出しをクラッシュさせるために ‘undefined’ というアトムを返す。(通常のErlangレジストリと同様です)
  • プロセスは2つの名前を持てない
  • 2つのプロセスは同じ名前を共有できない
  • 一度登録されたプロセスも、呼び出しの間に登録が外されたら、再度登録ができる
  • プロセスの登録を外す処理は決してクラッシュしない
  • 登録済みプロセスがクラッシュした場合は、その名前を登録から外す

これが準拠すべきリストです。 私がやったように、リストの項目を一つ一つ実装していくのではなく、各仕様を各々テストに変換していきました。 最終的に得られたものが 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 %%%
%%%%%%%%%%%%%%%%%%%%%%%%

上のテストの構成を見てくださいよ。 この時点でかなりぐっと良くなりました。 ファイルの先頭部分には、フィクスチャーと機能の最上位の説明書きだけが書いてあります。 次の部分には、必要となるであろうsetupとcleanup関数があります。 最後の部分にはテストセットを返すInstantiatorがあります。

この例の場合、Instantiatorは regis_server:start_link() が生成したプロセスが本当に生きているかどうかを確認し、 regis_server という名前で登録されたかを確認します。 もしこれが真なら、 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/0stop/1 以外のsetup関数やteardown関数をまったく必要としていない状況に気がつくでしょう。 このような理由から、 ?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))].
../_images/electrocardiogram.png

先ほど最初のバージョンのテストを観た時に説明されたように、このテストはプロセスが取得可能かどうかを確認します。 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 Instantiatorは15ミリ秒待ち(他のプロセスが確実に自身を登録するのを待つための短い遅延です)、その後Pidを受け取ってプロセスにメッセージを送るために、 regis_server から whereis 関数を使おうとします。 もしregisサーバが正しく機能していたら、メッセージは受け取られ、pidは関数の最後にあるテストにマッチするでしょう。

Don’t Drink Too Much Kool-Aid:

テストを読むことで、どうしてもしなければならなかったちょっとしたタイマーを使った作業が見て取れたと思います。 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のすべての整数を含んでいるべきです。 そして、登録済みプロセスをすべて削除することで片付けがおわります ― これらのプロセスをリークさせたくはないですから。

../_images/watch.png

テストが、テストセットで変数( L1L2 )を使う前に、そこに状態を保存する性質に気づいたことでしょう。 この理由は、返されたテストセットが、テスト初期化が実行された十分あとに、実行されるからです。 他のプロセスに依存する関数呼び出しと時間に影響を受けやすいイベントを ?_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 が行おうとしたプロセス呼び出しを実際にクラッシュするという仕様の想定を確認するものです。 このテストに関して、状態を保存するために一時変数を使う必要はまったくありません。両テストともに、私たちがregisサーバの状態を絶対に変更しなければ、サーバ稼働中はいつでも実施できます。

続けましょう:

{"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回登録できないことを確認しているだけです。

Note

上記のテストでは make_ref() を何度も使っていることに既にお気づきでしょう。 使えるのであれば、 make_ref() が行うような一意な値を生成する関数を使うのが便利です。 もし将来あるときに、誰かがテストを並行に走らせたくなった、あるいは絶対に止まらない単一のregisサーバ上で走らせたくなった場合、テストを修正することなくこうしたテストが行えるようになります。

もし、たとえばすべてのテストで 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つのプロセス(結果が必要ないプロセス)を生成して、その後に致命的になる処理を行なっています。

他方のプロセスが最初に名前を登録したあとに、テストプロセス自身がテストをする番になったら、期待したエラータプル( {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 では、そこに存在していないプロセスを取り除くときにクラッシュしないかどうかだけわかればよいのです。

ついに最後のテストとなりました。そしてここまでのプロセスを登録するテストの中で最も重要なテストです。クラッシュした名前付きプロセスの名前と登録取り消しするテストです。 これは非常に重要なことを示唆しています。なぜなら、名前を削除しなければ、レジストリサーバは膨張し続けて、名前の選択肢がどんどん狭くなってしまうからです:

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).

このコードは一連の流れで行われます:

  1. プロセスを登録します
  2. プロセスが登録されたことを確認します
  3. プロセスを殺します
  4. プロセス識別子を奪い取ります(完全にスパイのやり方です)
  5. 名前を確保できたか確認します

正直に言って、テストは簡潔に書けました:

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)).

死んだプロセスの識別子を奪い取る部分のコードは泥棒の空想にほかなりません。

これでおしまいです! 全部きちんと書けたら、コードをコンパイルしてテストスイートを実行できるはずです:

$ 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’オプションを追加することで、テストの説明と実行時の情報がレポートに追加されましたね。 すてきですね。

../_images/knit.png

27.6. EUnitで良い湯にっと

この商では、EUnitのほとんどの機能の使い方に触れ、テストスイートの実行の仕方を見てきました。 より重要なことは、並列プロセスに対するテストの書き方に関係するいくつかのテクニックについても、実世界で適用可能な例を使って、見てきたことです。

知っておくべき最後の技があります。 gen_servergen_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 Questのplayerモジュール のために書かれたテストを読むことをおすすめします。 そのテストでは、いま紹介したものとは異なるテクニックを使って gen_server をテストしています。そのテストでは handle_callhandle_casthandle_info に対する個々の関数呼び出しはすべて独立して行われます。 これらは依然としてテスト駆動で開発されていますが、テスト駆動が必要になることで物事の進め方を強制的に変えていきます。

いずれにせよ、プロセスレジストリを、すべてのErlangプロセスで利用可能なインメモリデータベースであるETSを使ったものに書きなおすときに、テストの真価を知ることとなります。