31. 珍しいテストのためのよくあるテスト

数章前に、単体テストとモジュールテスト、さらには並列のテストを行うためのEUnitの使い方を見て来ましtあ。 そのときは、EUnitの限界が見えていました。 設定の複雑さと、お互いにやり取りする必要のある長いテストでは、だんだんと問題が出てきました。 さらにEUnitには、新しく学んだ分散Erlangと、その能力をすべてを扱う機能はまったくありませんでした。 幸いにも、他のテストフレームワークが存在します。こちらは、いま私たちが行いたいような重いテストにより適しています。

../_images/black-box.png

31.1. Common Testとはなにか

プログラマーとして、プログラムをブラックボックスとして扱うことを楽しんでいます。 私たちの多くは、良い抽象化の裏にある核となる原則は、任意のブラックボックで実装したものを置換できること、と定義するでしょう。 箱のなかに何かを入れて、何かを取り出すのです。 欲しい物が得られる限り、中身がどう動作していようと気にしません。

テストの世界では、このことは、どのようにシステムをテストしたいのか、ということと重要な関係があります。 EUnitを扱っていたときは、モジュールを ブラックボックス として扱いました。エクスポートされた関数だけをテストし、エクスポートされていない中の実装を行なっている関数に対してはテストを行なっていません。 またProcess Questのプレーヤーモジュールでのテストのように、要素をホワイトボックスとしてテストする例も示しました。 これは、ボックス内の稼働する部分すべてのやり取りに関するテストを外から行うのは非常に複雑なため、必要な手法です。

いまの話はモジュールと関数に対するテストの話でした。 もう少しズームアウトしてみたらどうでしょうか。 より広い視点で見られるようにテストの対象を広げてみましょう。 ライブラリのテストをしたい場合はどうでしょう。 アプリケーション全体ではどうでしょう。 さらに広く考えて、システム全体をテストしたい場合はどうでしょうか。 そのときは、 システムテスト と呼ばれるものにより熟練したツールが必要になります。

EUnitはモジュールレベルでホワイトボックステストを行うには良いツールです。 EUnitはライブラリやOTPアプリケーションをテストする上では結構良いツールです。 システムテストやブラックボックステストを行うこともできますが、最適ではありません。

一方で、Common Testはシステムテストに非常に向いています。 ライブラリやOTPアプリケーションにも結構使えます。個々のモジュールをテストするときにも使えますが、最適ではありません。 テストするものが小さければ、EUnitの方が適している(そして、柔軟で、楽しい)でしょう。 テストが大きくなったら、Common Testがより適している(そして、柔軟で、あと、ちょっと楽しい)でしょう。

みなさんも、Common Testについて聞いたことがあって、Erlang/OTPに付いてきたドキュメントで理解しようとしたことでしょう。 それですぐに挫折することはよくあることです。 心配しないでください。 問題は、Common Testが非常に強力であり、したがってユーザガイドも長く、しかも本書を書いているときも、ほとんどのドキュメントは、Common TestがEriccson内部で使われていた時代の社内文章から来ているもののようだった、ということです。 事実、Common Testの公式ドキュメントは、チュートリアルというよりは、すでにそれを理解している人向けの参照マニュアルです。

Common Testを正しく学ぶために、Common Testの単純な部分から始め、それをゆっくりとシステムテストまで拡張していかなければいけません。

31.2. よくあるテストケース

手を動かす前に、Common Testがどのようにテストを行なっているかの概要を簡単にお伝えしましょう。 まず、Common Testはシステムテストに向いているものなので、次の2つのことを想定しています:

  1. テストに必要なものをインスタンス化するデータが必要です
  2. 私たちはだらしないので、すべての副作用を保存しておく場所が必要です

これらの理由から、Common Testは通常次のように体系化されています:

../_images/ct-struct.png

テストケースはもっとも簡潔なものです。 これは失敗か成功かのどちらかをするちょっとしたコードです。 もしテストケースがクラッシュしたら、テストは失敗です。(なんということでしょう(棒) そうでなければ、テストケースは成功です。 Common Testでは、テストケースは単一の関数です。 これらの関数はすべてテストスイート (3) の中にあり、これは関連するテストケースをひとまとめにしておくモジュールです。 個々のテストスイートは、テストオブジェクトディレクトリ (2) の中にあります。 テストルート (1) は、たくさんのテストオブジェクトディレクトリを包含しているディレクトリですが、OTPアプリケーションの性質上、しばしば個々に開発されていて、多くのErlangプログラマはこの層は省略する傾向にあります。

どのような場合でも、この体系を理解しているので、2つの想定の話に戻ることができます。(インスタンス化して、それらを散らかすこと) 各テストスイートは _SUITE で終わるモジュールです。 前の章の魔法の8ボールアプリケーションをテストする場合であれば、そのテストスイートは m8ball_SUITE と命名するでしょう。 テストスイートに関連して、 データディレクトリ と呼ばれるディレクトリがあります。 各スイートにつき1つこのディレクトリを持つことができて、通常は Module_SUITE_data/ という名前になります。 魔法の8ボールアプリケーションの場合は、 m8ball_SUITE_data となるでしょう。 このディレクトリには何を入れても良いです。

副作用に関してはどうでしょうか。 テストを何度も実施するので、Common Testは次のように構造をすこし拡張します:

../_images/ct-logs.png

Common Testは、テストを実行するときはいつでも、ログを保存する場所を探します。(通常、カレントディレクトリになりますが、のちほど設定方法をご紹介します。) ログを保存する際には、新しい一意なディレクトリを作成します。 データディレクトリ内にあるこのディレクトリ(上図の Priv Dir )は、テストのはじめに初期化する際に渡されます。 その後、そのプライベートなディレクトリにはなにを書き込んでも自由で、書き込んだあとには、何か重要なものや前のテスト結果を上書きしてしまうリスクなしに、結果を検証できます。

構造的な話は十分わかったので、そろそろ最初のサンプルテストスイートを書いても良い頃でしょう。 ct/ (なんでも好きな名前を付けてください。これは何をしてもいい場所です。)という名前のディレクトリを作成します。 その中に demo/ という名前のディレクトリを作成して、ここで例として簡潔なテストを作っていきます。 これはテストオブジェクトディレクトリになります。

テストオブジェクトディレクトリの中で、 basic_SUITE.erl という名前のモジュールを作成することから始めます。このテストスイートで、できるだけ基本的な事を確認します。 今回のテストには必要ないので、 basic_SUITE_data/ ディレクトリは作らなくても大丈夫です。 データディレクトリが無いからといってCommon Testは警告しません。

モジュールは次のようになります:

-module(basic_SUITE).
-include_lib("common_test/include/ct.hrl").
-export([all/0]).
-export([test1/1, test2/1]).

all() -> [test1,test2].

test1(_Config) ->
    1 = 1.

test2(_Config) ->
    A = 0,
    1/A.

1つ1つ学んでいきましょう。 まず最初に、 "common_test/include/ct.hrl" というファイルをインクルードしなければいけません。 このファイルはいくつか便利なマクロを提供してくれます。そしてたとえ basic_SUITE がそれを使わなかったとしても、このファイルをインクルードすることを癖にしとくといいと思います。

そのあと、 all/0 関数というものを書いています。 この関数はテストケースの一覧を返します。 これは基本的にはCommon Testに「ねえ、このテストケース実行してよ!」と伝えるためのものです。 EUnitでは名前( *_test() あるいは *_test_() )に基づいてテストをしました。Common Testでは、これを明示的に関数呼び出しをすることで行います。

../_images/priv_dir.png

ここにある _Config 変数はなんでしょうか。 これは今は使われていませんが、知識として、この変数はテストケース必要な初期状態が渡される、ということを覚えておいてください。 状態は実際はproplistで、最初は data_dirpriv_dir の2つの値を持っていて、これらは静的なデータ用に用意したディレクトリと、自由に使って良いディレクトリがそれぞれ渡されます。

テストはコマンドラインから、あるいはErlangシェル上で実行できます。 コマンドラインを使う場合は、 $ ct_run -suite Name_SUITE を呼び出すことで実行可能です。 (2011年12月ごろにリリースされた)R15よりも前のErlang/OTPであれば、デフォルトのコマンドは ct_run ではなく run_test でしょう。(システムによっては両方用意してあります) コマンド名は、他のアプリケーションと名前が衝突するリスクを最小限にするために、一般的な名前から少し変更されました。 テストを走らせてみると次のような結果になります:

ct_run -suite basic_SUITE
...
Common Test: Running make in test directories...
Recompile: basic_SUITE
...
Testing ct.demo.basic_SUITE: Starting test, 2 test cases

- - - - - - - - - - - - - - - - - - - - - - - - - -
basic_SUITE:test2 failed on line 13
Reason: badarith
- - - - - - - - - - - - - - - - - - - - - - - - - -

Testing ct.demo.basic_SUITE: *** FAILED *** test case 2 of 2
Testing ct.demo.basic_SUITE: TEST COMPLETE, 1 ok, 1 failed of 2 test cases

Updating /Users/ferd/code/self/learn-you-some-erlang/ct/demo/index.html... done
Updating /Users/ferd/code/self/learn-you-some-erlang/ct/demo/all_runs.html... done

ここでは2つのテストケースの内1つが失敗していることが分かりました。 また、大量のHTMLファイルを引き継いでいることが分かります。 これが何かを見る前に、Erlangシェルからテストを実行する方法を見てみましょう:

$ erl
...
1> ct:run_test([{suite, basic_SUITE}]).
...
Testing ct.demo.basic_SUITE: Starting test, 2 test cases

- - - - - - - - - - - - - - - - - - - - - - - - - -
basic_SUITE:test2 failed on line 13
Reason: badarith
- - - - - - - - - - - - - - - - - - - - - - - - - -
...
Updating /Users/ferd/code/self/learn-you-some-erlang/ct/demo/index.html... done
Updating /Users/ferd/code/self/learn-you-some-erlang/ct/demo/all_runs.html... done
ok

出力結果を少々削除しましたが、コマンドライン版と同じ結果を返します。 では、これらのHTMLファイルに何が書いてあるのか見てみましょう:

$ ls
all_runs.html
basic_SUITE.beam
basic_SUITE.erl
ct_default.css
ct_run.NodeName.YYYY-MM-DD_20.01.25/
ct_run.NodeName.YYYY-MM-DD_20.05.17/
index.html
variables-NodeName

おや、Common Testは私の美しいディレクトリに一体なにをしたんでしょうか。 なんともけしからんものですね。 ここに2つのディレクトリがあります。 面白そうだと思ったらご自由にそのディレクトリの中身を見てもらって構いませんが、私のような臆病者は代わりに all_runs.htmlindex.html を見るようがいいです。 前者は、実行したすべてのテストのイテレーションの索引にリンクしていて、後者は最新のテストだけにリンクしています。 どちらかを選んで、上記2つのテストを含むテストスイートを見つけるまで、ブラウザ内で色々とクリックして(入力デバイスとしてマウスを信用していないならキーを入力して)みましょう:

../_images/ct-log-screen.png

test2 が失敗したのがわかると思います。 下線が引いてある数字をクリックすると、モジュールの生のソースが見れます。 代わりに test2 のリンクをクリックすると、何が起きたかについて詳細なログが見られます:

=== source code for basic_SUITE:test2/1
=== Test case started with:
basic_SUITE:test2(ConfigOpts)
=== Current directory is "Somewhere on my computer"
=== Started at 2012-01-20 20:05:17
[Test Related Output]
=== Ended at 2012-01-20 20:05:17
=== location [{basic_SUITE,test2,13},
              {test_server,ts_tc,1635},
              {test_server,run_test_case_eval1,1182},
              {test_server,run_test_case_eval,1123}]
=== reason = bad argument in an arithmetic expression
  in function  basic_SUITE:test2/1 (basic_SUITE.erl, line 13)
  in call from test_server:ts_tc/3 (test_server.erl, line 1635)
  in call from test_server:run_test_case_eval1/6 (test_server.erl, line 1182)
  in call from test_server:run_test_case_eval/9 (test_server.erl, line 1123)

ログを見ることで、何に失敗したのかを正確に把握することができ、Erlangシェルに表示されたどのログよりもずっと詳細に見ることができます。 このことを知らないと、あなたがシェルユーザだった場合に、Common Testはものすごく使いづらいと思ってしまうので、これは非常に大事な事実です。 もしあなたがGUIの方が好きな人であれば、これはかなり楽しいと思いますよ。

すばらしいHTMLファイルを見て回るのでも十分ですが、より細かい統計データの取得方法について見てみましょう。

Note

タイムマシンなしで過去にタイムトラベルしたいと思ったら、R15B以前のErlangをダウンロードして、それでCommon Testを使ってみましょう。 ブラウザを見るとログのスタイルがまるで1990年代後半のような見た目になっていることに驚くでしょう。

31.3. 状態付きでテストする

もしEUnitの章を読んだのであれば(そして流し読みしていないのであれば)、EUnitには フィクスチャー と呼ばれるものがあったのを覚えているでしょう。これはテストケースを特別なインスタンス化(setup)を行い、テストケースの前後にそれぞれ呼び出されるteardownコードを設定するものでした。

Common Testでもこのコンセプトに従います。 EUnit形式のフィクスチャーではなく、代わりに2つの関数に依存します。 1つ目は init_per_testcase/2 と呼ばれる、setup関数です。そして2つ目は end_per_testcase/2 と呼ばれるteardown関数です。 これらの使い方を見るために、 state_SUITE という新しテストケースを( demo/ ディレクトリ下に)作り、次のコードを追加しましょう:

-module(state_SUITE).
-include_lib("common_test/include/ct.hrl").

-export([all/0, init_per_testcase/2, end_per_testcase/2]).
-export([ets_tests/1]).

all() -> [ets_tests].

init_per_testcase(ets_tests, Config) ->
    TabId = ets:new(account, [ordered_set, public]),
    ets:insert(TabId, {andy, 2131}),
    ets:insert(TabId, {david, 12}),
    ets:insert(TabId, {steve, 12943752}),
    [{table,TabId} | Config].

end_per_testcase(ets_tests, Config) ->
    ets:delete(?config(table, Config)).

ets_tests(Config) ->
    TabId = ?config(table, Config),
    [{david, 12}] = ets:lookup(TabId, david),
    steve = ets:last(TabId),
    true = ets:insert(TabId, {zachary, 99}),
    zachary = ets:last(TabId).

これは、通常のETSに関する小さなテストで、順序付きセットのいくつかのコンセプトを確認しています。 ここで面白いのは、2つの新しい関数である init_per_testcase/2end_per_testcase/2 です。 両関数ともに、呼び出されるためにエクスポートされる必要があります。 エクスポートされると、これらの関数はモジュール内の すべての テストケースで呼び出されます。 関数を引数に基づいて分割することも可能です。 2つの引数の内、前者はテストケース名(アトム)で、後者は編集可能な Config proplist です。

Note

Config から読み込むために、 proplist:get_value/2 を使うのではなく、Common Testではインクルードファイル内にある ?config(Key, List) マクロを使ってキーに対応する値を取得しています。 このマクロは事実 proplist:get_value/2 のラッパーで、ドキュメントでもそのように説明されています。したがって、 Config を壊してしまうのではないかという心配をする必要なくproplistとして扱って問題無いです。

例として、 abc というテストがあって、最初の2つのテストにだけsetup関数とteardown関数が必要だった場合、init関数は次のようになるでしょう:

init_per_testcase(a, Config) ->
    [{some_key, 124} | Config];
init_per_testcase(b, Config) ->
    [{other_key, duck} | Config];
init_per_testcase(_, Config) ->
    %% ignore for all other cases
    Config.

end_per_testcase/2 関数も同様です。

state_SUITE を振り返ると、テストケースは確認できますが、注目すべきはETSテーブルの初期化方法です。 後継者を1つも指定せず、それでもなお、テストはinit関数が終わった後に問題なく実行されました。

ETSの章 学んだことを思い出すと、ETSテーブルは通常それを起動したプロセスに所有されます。 この例の場合、私はテーブルをそのままにしました。 テストを実行すれば、テストスイートが成功するのが確認できるでしょう。

ここから推測できることは、あるプロセス内で init_per_testcase 関数と end_per_testcase 関数はテストケース自身として稼働している、ということです。 これによって、安全にリンクを張ったり、テーブルを起動したりといったことを、違うプロセスが壊してしまうのではないかという心配なしに行うことができます。 テストケース中のエラーに関してはどうでしょうか。 幸いにも、テストケース中でクラッシュしても、 kill 終了シグナルが無い限りは、Common Testが後片付けをして end_per_testcase 関数を呼び出す途中で止まってしまうことはありません。

ここまでで、少なくとも柔軟性の観点からは、EUnitとCommon Testをほぼ同様に使えるようになりました。 まだ素敵なassertマクロについては触れていませんが、Common Testには、しゃれたレポートやよく似たフィクスチャーや、まっさらな状態から何を書き込んでもいいプライベートなディレクトリがあります。 さらに何が必要でしょうか。

Note

デバッグしたりテストの進捗を見せるだけのために出力したいだけであれば、 io:format/1-2 がHTMLログにのみ出力して、Erlangシェルには出力しないことにすぐに気がつくでしょう。 もし両方を行いたい(タイムスタンプも表示したい)のであれば、 ct:pal/1-2 関数を使いましょう。 これは io:format/1-2 と同様に動作しますが、シェルとログの両方に出力します。

31.4. テストグループ

いまのところ、私たちのテストスイート内のテストの構造はせいぜい次のような見た目でしょう:

../_images/ct-cases.png

もし、init関数が必要とするものは似ているけれど、いくつか違う点があるというようなテストケースがたくさんあった場合にはどうしたらいいでしょうか。 コピペして修正するというのは簡単ですが、維持するのが本当に苦痛になるでしょう。

さらに、テストで行いたいことが並行で走らせることであったり、順序良く走らせるのではなく順不同に走らせたい場合にはどうしたら良いでしょうか。 この場合、これまで見てきた方法ではこれを簡単に行う方法はありませんでした。 これはEUnitでの限界を見せた問題とほぼ同様のものです。

これらの問題を解決するために、テストグループと呼ぶものを導入します。 Common Testのテストグループではいくつかのテストを階層状にグループ分けすることができます。 さらに、あるグループを他のグループ内にグループ分けすることもできます。

../_images/ct-groups.png

グループ分けするために、まずグループを宣言剃る必要があります。 グループの宣言方法は、グループ関数を追加して、すべてのグループを宣言するという方法です:

groups() -> ListOfGroups.

groups() 関数があります。 ここで ListOfGroups は次のようなものであるべきでしょう:

[{GroupName, GroupProperties, GroupMembers}]

さらに詳細に言えば、次のようになっています:

[{test_case_street_gang,
  [],
  [simple_case, more_complex_case]}].

これは小さなテストケース「ストリートギャング」の場合です。 より複雑な例は次のとおりです:

[{test_case_street_gang,
  [shuffle, sequence],
  [simple_case, more_complex_case,
   emotionally_complex_case,
   {group, name_of_another_test_group}]}].

この例では shufflesequence という2つの性質を明記しています。 その意味はすぐに学びます。 また、この例はあるグループが他のグループを含んでいる例も見せています。 グループ関数は次のようになるでしょう:

groups() ->
    [{test_case_street_gang,
      [shuffle, sequence],
      [simple_case, more_complex_case, emotionally_complex_case,
       {group, name_of_another_test_group}]},
     {name_of_another_test_group,
      [],
      [case1, case2, case3]}].

また、グループを他のグループの中にインラインで定義することもできます:

[{test_case_street_gang,
  [shuffle, sequence],
  [simple_case, more_complex_case,
   emotionally_complex_case,
   {name_of_another_test_group,
    [],
    [case1, case2, case3]}
 ]}].

これは少し複雑ですよね。 注意深く読んでいれば、時間が経つにつれ簡潔に見えてきます。 いずれにせよ、入れ子になったグループは必須ではないですし、紛らわしいと思ったら使わないこともできます。

しかし待ってください、このようなグループをどのように使ったらいいのでしょうか。 これを all/0 関数に入れましょう:

all() -> [some_case, {group, test_case_street_gang}, other_case].

このようにすることで、Common Testはテストケースを実行すべきか否かを判断できます。

グループプロパティについての話を飛ばしてしまいました。 shufflesequence と空のリストについては見てきましたね。 これらが何を意味するのかを次に示します:

空のリスト / オプションなし:
 グループ内のテストケースは順番に実行されます。 テストが失敗したら、リストの残りのテストが実行されます。
shuffle:テストを順不同に実行します。 シーケンスに使われたランダムシード(初期値)はHTMLログに出力され、 {A,B,C} という形式で表示されます。 特定のシーケンスでのテストが失敗して、それを再現したい場合、HTMLログ内にあるシードを使って、 shuffle オプションを変更して {shuffle, {A,B,C}} としましょう。 これで必要があれば、ランダムさを再現して、特定の順番で実行できます。
parallel:テストは異なるプロセス上で実行されます。 init_per_group 関数と end_per_group 関数をエクスポートし忘れると、Common Testはこのオプションを黙って無視するので、注意して下さい。
sequence:これは必ずしもテストを順番に実行することを意味するのではなく、テストがグループのリスト内で失敗した場合には残りのテストが飛ばされるということを意味します。 このオプションは shuffle と組み合わせて使うことができ、ランダムテストで失敗したら残りのテストを止めたいときに有用です。
{repeat, Times}:
 グループを Times 回繰り返します。 したがって、テストケースのシーケンス全体を並行に9回実行したい場合は、グループプロパティを [parallel, {repeat, 9}] と設定すれば良いです。 また Timesforever という値を渡すこともできます。とはいっても、ハードウェア障害や宇宙の熱死(エヘン)には勝てないので、「永遠に」というとウソになりますが。
{repeat_until_any_fail, N}:
 テストの内どれか1つが失敗するまで、あるいはすべてのテストが N 回実行されるまで、すべてのテストを実行します。 Nforever にすることも可能です。
{repeat_until_all_fail, N}:
 上のプロパティと同様ですが、すべてのテストケースが失敗するまでテストを実行する点で異なります。
{repeat_until_any_succeed, N}:
 上のプロパティと同様ですが、少なくとも1つのテストケースが成功するまでテストを実行する点で異なります。
{repeat_until_all_succeed, N}:
 もうこれが何かはお分かりだと思いますが、念のため説明すると、これも上のオプションと同様ですが、すべてのテストケースが成功するまでテストを実行する点が異なります。

すごいですね。 正直に言って、これはテストグループの中身の一部で、そろそろ例を示したほうがいい気がしてきました。

../_images/shuffling.png

31.5. 会議室

テストグループの最初の例として、会議室予約モジュールを作ろうと思います。

-module(meeting).
-export([rent_projector/1, use_chairs/1, book_room/1,
         get_all_bookings/0, start/0, stop/0]).
-record(bookings, {projector, room, chairs}).

start() ->
    Pid = spawn(fun() -> loop(#bookings{}) end),
    register(?MODULE, Pid).

stop() ->
    ?MODULE ! stop.

rent_projector(Group) ->
    ?MODULE ! {projector, Group}.

book_room(Group) ->
    ?MODULE ! {room, Group}.

use_chairs(Group) ->
    ?MODULE ! {chairs, Group}.

この基本的な関数は中央のレジストリプロセスを呼び出します。 これらの関数で、会議室を予約したり、プロジェクターを借りたい、椅子の使用権を予約したりすることができます。 演習なので、私たちはひどく複雑な組織構造をした大きな組織に属しているとします。 この組織構造のせいで、プロジェクターと会議室と椅子に対して、3つの異なる人物がそれぞれ責任者となっていますが、予約をするレジストリは中央に1つしかありません。 このような状況なので、すべての項目を1度に予約することができず、3通別々のメッセージを送らなければいけません。

誰が何を予約するかを知るために、レジストリにメッセージを1通送ってすべての値を取得することができます:

get_all_bookings() ->
Ref = make_ref(),
?MODULE ! {self(), Ref, get_bookings},
receive
    {Ref, Reply} ->
        Reply
end.

レジストリ自身はこのようになっています:

loop(B = #bookings{}) ->
receive
    stop -> ok;
    {From, Ref, get_bookings} ->
        From ! {Ref, [{room, B#bookings.room},
                      {chairs, B#bookings.chairs},
                      {projector, B#bookings.projector}]},
        loop(B);
    {room, Group} ->
        loop(B#bookings{room=Group});
    {chairs, Group} ->
        loop(B#bookings{chairs=Group});
    {projector, Group} ->
        loop(B#bookings{projector=Group})
end.

これでおしまいです。 うまく会議にこぎつけるために予約をするには、次の呼び出しを無事に行う必要があります:

1> c(meeting).
{ok,meeting}
2> meeting:start().
true
3> meeting:book_room(erlang_group).
{room,erlang_group}
4> meeting:rent_projector(erlang_group).
{projector,erlang_group}
5> meeting:use_chairs(erlang_group).
{chairs,erlang_group}
6> meeting:get_all_bookings().
[{room,erlang_group},
 {chairs,erlang_group},
 {projector,erlang_group}]

すばらしい。 しかし、これは間違っているように見えます。 おそらく何かが間違っているという引っかかる気持ちがあることでしょう。 多くの場合、3回の呼び出しを十分速く行うことができれば、会議室に必要な物を問題なく取得することが出来るでしょう。 もし2人が同時にこの作業を行なって、各呼び出しの間に小休憩があったら、2つ(以上)のグループが同じ設備を同時に借りようとするということが起きてしまうかもしれません。

これではだめです! プログラマーがプロジェクターを借りて、一方で取締役会が会議室を押さえて、そして人事部がすべての椅子を借りてしまう、なんてことが起きてしまいます。 設備が全部貸し出されているのに、誰も有効に使うことができないのです!

この問題を解決することに関して心配することはありません。 かわりに、Common Testのテストスイートの形でデモをお見せしましょう。

このテストスイートは meeting_SUITE.erl という名前で、予約をめちゃくちゃにしてしまう競合状態を引き起こす単純な考えに基づいています。 ここでは3つのテストケースを書き、それぞれがグループを表します。 Carlaは女性を、Markは男性を表し、犬(dog)はどういうわけか人間が作った道具を使って会議を開きたいと思った動物の一群を表します。

-module(meeting_SUITE).
-include_lib("common_test/include/ct.hrl").

...

carla(_Config) ->
    meeting:book_room(women),
    timer:sleep(10),
    meeting:rent_projector(women),
    timer:sleep(10),
    meeting:use_chairs(women).

mark(_Config) ->
    meeting:rent_projector(men),
    timer:sleep(10),
    meeting:use_chairs(men),
    timer:sleep(10),
    meeting:book_room(men).

dog(_Config) ->
    meeting:rent_projector(animals),
    timer:sleep(10),
    meeting:use_chairs(animals),
    timer:sleep(10),
    meeting:book_room(animals).

ここでは、これらのテストが実際に何かをテストするかどうかは気にしません。 これらのテストはミーティングモジュールを使うためだけに存在し(すぐにテストの正しい使い方についても触れます)、そして間違った予約の生成を試みます。

これらのテストで競合状態になったかどうかを判断するためには、4つ目または最後のテストで meeting:get_all_bookings() 関数を使います:

all_same_owner(_Config) ->
    [{_, Owner}, {_, Owner}, {_, Owner}] = meeting:get_all_bookings().
../_images/dog-meeting.png

この例では、予約可能な各設備の予約者に関するパターンマッチを行なって、実際にすべての設備が同じ予約者になっているかを確認します。 ちゃんとした会議を行うのであれば、同じ予約者になっていることが望ましいでしょう。

ファイル内にある4つのテストケースを実際に動かせるようにするにはどうしたら良いでしょうか。 テストグループを賢く使う必要があります。

まず最初に、競合状態を作り出したいので、大量のテストを並行に走らせる必要があります。 次に、これらの競合状態での問題を確認するための要件があるとして、失敗の度に何度も all_same_owner 実行するか、あるいは最後に1回だけ残念な状況になっているのを確認するか、どちらかを行う必要があります。

ここでは後者を選びます。 後者の場合は、次のようになるでしょう:

all() -> [{group, clients}, all_same_owner].

groups() -> [{clients,
              [parallel, {repeat, 10}],
              [carla, mark, dog]}].

この例では clients テストグループを作成して、その中で個別のテストである carlamarkdog を呼んでいます。 これらのテストは並行に10回ずつ実施されます。

ここで all/0 関数の中でグループを呼んでから、 all_same_owner を呼んでいるのが分かるでしょう。 これは、デフォルトではCommon Testはテストやグループを宣言された順に実行するからです。

しかし待って下さい。 meeting プロセス自身の起動と停止を忘れていました。 そのためには、テストが ‘clients’ グループにあるかどうかは関係なく、全テストが実施されている間、 meeting のプロセスを生かし続けておく方法を考える必要があります。 この問題の解法は、テストを入れ子にして、他のグループの中に入れて、1階層深くすることです。

all() -> [{group, session}].

groups() -> [{session,
              [],
              [{group, clients}, all_same_owner]},
             {clients,
              [parallel, {repeat, 10}],
              [carla, mark, dog]}].

init_per_group(session, Config) ->
    meeting:start(),
    Config;
init_per_group(_, Config) ->
    Config.

end_per_group(session, _Config) ->
    meeting:stop();
end_per_group(_, _Config) ->
    ok.

init_per_group 関数と end_per_group 関数を使って、 session グループ( {group, clients}all_same_owner を実行します)が会議に関するやり取りをするように明記します。 これら2つのsetup関数とteardown関数をエクスポートし忘れないようにしてください。そうしないと何一つ並行に動作しません。

では、テストを実行してどうなるか見てみましょう:

1> ct_run:run_test([{suite, meeting_SUITE}]).
...
Common Test: Running make in test directories...
...
TEST INFO: 1 test(s), 1 suite(s)

Testing ct.meeting.meeting_SUITE: Starting test (with repeated test cases)

- - - - - - - - - - - - - - - - - - - - - - - - - -
meeting_SUITE:all_same_owner failed on line 50
Reason: {badmatch,[{room,men},{chairs,women},{projector,women}]}
- - - - - - - - - - - - - - - - - - - - - - - - - -

Testing ct.meeting.meeting_SUITE: *** FAILED *** test case 31
Testing ct.meeting.meeting_SUITE: TEST COMPLETE, 30 ok, 1 failed of 31 test cases
...
ok

面白いですね。 問題は、3つのタプルで異なる設備が異なる人々に予約されているというbadmatchだと言っています。 さらに、この出力では、 all_same_owner テストで失敗したと伝えてくれます。 all_same_owner が計画通りクラッシュしたのはかなり幸先が良いと思います。

HTMLログを見にいくと、まさに失敗したテストで何が実行されたか、そしてどのような理由で失敗したかを見ることができます。 テスト名をクリックして、正しいテスト結果を見ることができます。

Note

テストグループの話題を終わる前に最後に(かつ非常に重要な事を)1つ知っておくべきは、テストケースのinit関数はテストケースのプロセスと同じプロセス内で動作しますが、テストグループのinit関数はテストとは異なるプロセスで動作します。 つまり、生成元のプロセスにリンクされたアクターを初期化するときはいつでも、まず最初に確実にリンクを解除しなければいけないということです。 ETSテーブルの場合は、確実に消えることのない後継プロセスを定義しなければなりません。 プロセスに紐付いた他のコンセプト(ソケット、ファイルディスクリプタ等々)についても同様です。

31.6. テストスイート

階層の観点から、グループやどのように実行するかといった操作を入れ子にするよりも良いテストスイートに何が追加できるでしょうか。 できることはあまり多くはありませんが、テストスイート自身に階層構造を追加してみましょう:

../_images/ct-suite.png

ここで2つの関数を追加します。 init_per_suite(Config)end_per_suite(Config) です。 これらは他のinit関数やend関数と同様に、データやプロセスの初期化に関してより制御できるようにすることが狙いです。

init_per_suite/1 関数と end_per_suite/1 関数は1度だけ実行され、それぞれすべてのグループまたはテストケースの前後に実行されます。 これらの関数はたいてい、一般的な状態やテストに必要になる依存関係を扱うときに便利です。 例えば、依存するアプリケーションを手動で起動するといったことも含まれています。

31.7. テストスペック

テストを実行し終わったあとにテストディレクトリを見ると、かなりうっとうしいものがあることでしょう。 ログ用の大量のファイルがディレクトリ内に散乱しています。 CSSファイルやHTMLログ、ディレクトリ、テスト実行履歴などです。 これらを1つのディレクトリ内に保存できる良い方法があったらかなり素敵でしょう。

ほかにも、これまでテストスイートからテストを実行してきました。 これまでで、多くのテストスイートを1度に実行したり、あるいは1つの(または多くの)テストスイートから幾つかのテストケースまたはテストグループだけを実行する良い方法はありませんでした。

もちろん、ここでこの話題に触れているのは、これらの問題の解決方法があるからです。 コマンドラインとErlangシェルの両方からこういったことを行うことができますし、その方法は ct_run のドキュメントでも確認できます。 しかしながら、テストを実行する度に手動ですべてを指示する代わりに、テストスペックと呼ばれるものについて見ていきましょう。

../_images/ct-specs.png

テストスペックは、どのようにテストを実行したいかについてなんでも細かく指示できる特別なファイルで、Erlangシェルやコマンドラインからも呼び出すことができます。 テストスペックはどのような拡張子でも構いません。(個人的には .spec ファイルとしています) specファイルには、consultファイルのように、Erlangタプルが書いてあります。 specファイルに指定することができる項目について一部載せておきます:

{include, IncludeDirectories}:
 Common Testが自動的にテストスイートをコンパイルするときに、このオプションでインクルードファイルファイルがどこにあるかを指定することができます。 IncludeDirectories の値はstring(リスト)もしくはstringのリスト(リストのリスト)でなければなりません。
{logdir, LoggingDirectory}:
 ログを取るときに、すべてのログは LoggingDirectory に移されます。この値はstringです。 テストが実行される前に指定されたディレクトリが存在しなければならないことに留意して下さい。 さもなければCommon Testは警告をします。
{suites, Directory, Suites}:
 Directory 内から指定されたテストスイートを探します。 Suites はアトム( some_SUITE )、アトムのリスト、またはディレクトリ内のすべてのテストスイートを実行する場合は all を指定できます。
{skip_suites, Directory, Suites, Comment}:
 事前に宣言されたものからテストスイートのリストを取り除いて、指定されたテストスイートを飛ばします。 引数 Comment は、なぜこのテストを飛ばすことにしたかを説明するstringです。 このコメントは最終的なHTMLログに記録されます。 表には黄色く ‘SKIPPED: Reason’ と、 Reason の部分には Comment に書かれていたことがそのまま出力されます。
{groups, Directory, Suite, Groups}:
 このオプションは指定されたテストスイートからいくつかのテストグループだけを選びます。 Groups 変数は1つのアトム(グループ名)、もしくはすべてのグループを指定する場合は all と指定します。 値はより複雑にすることも可能で、 groups() 内のテストグループの定義を上書きして、 {GroupName, [parallel]} とテストを再コンパイルする必要なく GroupName のオプションを parallel にすることもできます。
{groups, Directory, Suite, Groups, {cases,Cases}}:
 1つ上のオプションに似ていますが、テスト内に含めるべきテストケースを Cases を1つのテストケース名(アトム)、テストケース名のリスト、あるいは all というアトムで置き換えることで指定できます。
{skip_groups, Directory, Suite, Groups, Comment}:
 このコマンドはR15Bで追加され、R15B01でドキュメントが追加されました。 これによって、テストスイートでの skip_suites のように、テストグループを飛ばすことができます。 R15B以前にはなぜこれがなかったのか、説明はありません。
{skip_groups, Directory, Suite, Groups, {cases,Cases}, Comment}:
 1つ上のオプションに似ていますが、さらに特定のテストケースも飛ばすことができます。 R15B以降でのみ利用可能です。
{cases, Directory, Suite, Cases}:
 指定したテストスイートから特定のテストケースのみ実行します。 Cases はアトム、アトムのリスト、または all です。
{skip_cases, Directory, Suite, Cases, Comment}:
 skip_suites に似ていますが、特定のテストケースだけを飛ばす点が異なります。
{alias, Alias, Directory}:
 ディレクトリ名を全部書くのは非常に面倒なので(特にフルパスの場合)、Common Testではエイリアス(アトム)を設定することができます。 簡潔に書きたい時に非常に便利です。

簡単な例をお見せする前に、 logs/ ディレクトリを demo/ ディレクトリ(私の手元では ct/ )の上に追加して下さい。 当然、ここにCommon Testのログを移すことになります。 これまでのテストに使えるテストスペックは次の様になります。名前は spec.spec とでもしておきましょう:

{alias, demo, "./demo/"}.
{alias, meeting, "./meeting/"}.
{logdir, "./logs/"}.

{suites, meeting, all}.
{suites, demo, all}.
{skip_cases, demo, basic_SUITE, test2, "This test fails on purpose"}.

このspecファイルでは2つのエイリアス、 demomeeting を宣言しています。これらは、2つのテストディレクトリを指しています。 ログは、作ったばかりのディレクトリ ct/logs/ に保存します。 それから、meetingディレクトリにあるすべてのテストスイート、つまり偶然にも名前が一致している meeting_SUITE というテストスイートを実行するように記載します。 次に書いてあるのは、demoディレクトリにある2つのテストスイートに関してです。 さらに、 basic_SUITE テストスイート内の test2 を 飛ばすように書いています。なぜなら、これは失敗するとわかっているゼロ割の操作が含まれているからです。

$ ct_run -spec spec.spec (あるいはErlangのバージョンがR15以前であれば run_test )もしくは、Erlangシェル内で ct:run_test([{spec, "spec.spec"}]). 関数を使って、テストを実行します:

Common Test: Running make in test directories...
...
TEST INFO: 2 test(s), 3 suite(s)

Testing ct.meeting: Starting test (with repeated test cases)

- - - - - - - - - - - - - - - - - - - - - - - - - -
meeting_SUITE:all_same_owner failed on line 51
Reason: {badmatch,[{room,men},{chairs,women},{projector,women}]}
- - - - - - - - - - - - - - - - - - - - - - - - - -

Testing ct.meeting: *** FAILED *** test case 31
Testing ct.meeting: TEST COMPLETE, 30 ok, 1 failed of 31 test cases

Testing ct.demo: Starting test, 3 test cases
Testing ct.demo: TEST COMPLETE, 2 ok, 0 failed, 1 skipped of 3 test cases

Updating /Users/ferd/code/self/learn-you-some-erlang/ct/logs/index.html... done
Updating /Users/ferd/code/self/learn-you-some-erlang/ct/logs/all_runs.html... done

もしログを見る時間があれば、異なるテストをいくつか実行したあとには2つのディレクトリがあることが確認出来ると思います。 1つは失敗したテスト結果を保存するためのディレクトリで、予想通り失敗した会議が集まっています。 もう1つは成功したテスト結果と飛ばされたテストケースを保存するためのもので、 1 (1/0) の形式で保存します。 一般的にログの形式は TotalSkipped (IntentionallySkipped/SkippedDueToError) となっています。 specファイルで指定されたため飛ばされたものに関しては左側の数字になります。 init関数が失敗したために飛ばされたものに関しては右側の数字になります。

Common Testがかなり優秀なテストフレームワークに思えてきたことでしょう。しかし分散プログラミングの知識を使ってそれを適用できるとかなり素晴らしいテストフレームワークですよね。

../_images/ct-large-scale.png

31.8. 大規模テスト

Common Testでは分散テストのサポートもしています。 興奮して大量のコードを書いてしまう前に、どんな機能が提供されているのかを確認しましょう。 それほど多くはありません。 要点は、Common Testでは多くの異なるノード上でテストを起動できますが、動的にこれらのノードを起動させて、お互いを見させる事もできるということです。

このような機構なので、Common Testの分散機能は、多くのノード上で並行して動作する大きなテストスイートがある場合には、非常に便利です。 こうした機能はしばしば時間の節約であったり、あるいはコードが異なるコンピュータ上で動作しているプロダクション環境にあるので、試してみる価値があります。とくに後者の状況を反映した自動テストがあることは望ましいでしょう。

テストが分散になる場合、Common Testでは他のノードすべてを管理する中央ノード( CTマスター )が必要になります。 すべてがそのノードから始まり、ノードを起動して、テストを実行するよう命令して、ログを回収して、といった事を行います。

分散でテストを行うためにまず最初に行うことは、テストが分散できるようにテストスペックを拡張することです。 いくつかのタプルを追加します:

{node, NodeAlias, NodeName}:
 テストスイート、テストグループ、テストケースでの {alias, AliasAtom, Directory} と似たものですが、これはノード名に使われます。 NodeAliasNodeName はともにアトムである必要があります。 このタプルは、 NodeName が長いノード名である必要があり、場合によっては長くなりすぎるため、特に便利です。
{init, NodeAlias, Options}:
 これはより複雑なものです。 これはノードを起動させるためのオプションです。 NodeAlias は1つのノードエイリアス、あるいはノードエイリアスのリストです。 Optionct_slave モジュールで利用できるものです:

利用できるオプションをいくつか挙げてみます:

{username, UserName} と {password, Password}:
 NodeAlias で与えられたノードのホスト部分を使って、Common Testは与えられたホストにSSH(ポート番号 22)でユーザ名とパスワードを使って接続を試みて、そこから実行します。
{startup_functions, [{M,F,A}]}:
 このオプションは、他のノードが起動されたらすぐに呼ばれる関数の一覧を定義しています。
{erl_flags, String}:
 このオプションは、 erl アプリケーションを起動する際に渡したい通常のフラグを設定します。 たとえば、ノードを erl -env ERL_LIBS ../ -config conf_file という具合に起動したい場合は、オプションは {erl_flags, "-env ERL_LIBS ../ -config config_file"} となります。
{monitor_master, true | false}:
 もしCTマスターが停止して、このオプションが true に設定されていたら、スレーブノードも一緒に停止されます。 リモートノードを生成している場合には、このオプションを使うことをおすすめします。 そうしないと、マスターが死んだあとにもバックグラウンドでリモートノードが稼働し続けてしまいます。 さらに、テストを再度実行すると、Common Testは生き残っていたリモードノードに接続できてしまい、変な状態がこれらのノードに付与されてしまいます。
{boot_timeout, Seconds},:
 
{init_timeout, Seconds},:
 
{startup_timeout, Seconds}:
 これら3つのオプションで、リモートノードの異なる起動箇所を待つことができます。 bootタイムアウトは、ノードにping可能になるまでの時間で、デフォルト値は3秒です。 initタイムアウトは新しいリモートノードがCTマスターに起動したことを伝えるために内部的なタイマーで、デフォルト値は1秒です。 最後にstartupタイムアウトはComont Testに、 startup_functions タプル内で定義した関数をどれだけ待つかを伝えます。
{kill_if_fail, true | false}:
 このオプションは3つのタイムアウトの1つに反応するものです。 もしどれか1つでも条件に該当したら、Common Testは接続を中止し、テストを飛ばたりしますが、オプションが true に設定されていた場合、必ずしもノードは殺しません。 幸い、 true がデフォルト値になっています。

Note

これらのオプションはすべて ct_slave モジュールから提供されています。 インターフェースに正しく準拠していれば、スレーブノードを起動するために独自のモジュールを定義することも可能です。

これらのオプションのおかげで、リモートノードに対して多くの事ができるようになり、これがCommon Testに分散環境で力を発揮できるようにしているものの一部です。リモートノードを起動するときに、シェル上で手で行なっていたのと同様の制御を行うことができます。 さらに、ノードの起動用のものではないですが、分散テスト向けのオプションがもっとあります。

{include, Nodes, IncludeDirs}
{logdir, Nodes, LogDir}
{suites, Nodes, DirectoryOrAlias, Suites}
{groups, Nodes, DirectoryOrAlias, Suite, Groups}
{groups, Nodes, DirectoryOrAlias, Suite, GroupSpec, {cases,Cases}}
{cases, Nodes, DirectoryOrAlias, Suite, Cases}
{skip_suites, Nodes, DirectoryOrAlias, Suites, Comment}
{skip_cases, Nodes, DirectoryOrAlias, Suite, Cases, Comment}

ノードを引数に取って詳細を追加できる点以外は、これまで見てきたオプションとほとんど一緒です。 このようにして、あるテストスイートを特定のノードで実行し、他のテストスイートを異なるノードで実行する、といったことができます。 これは異なるノードが異なる環境やシステムの部位(データベース、外部アプリケーションなど)をシステムテストする場合に便利です。

これがどのように使えるのかを簡単に観るために、先ほどの spec.spec ファイルを分散テスト用に変更してみましょう。 dist.spec という名前でコピーし、次のように変更してみましょう:

{node, a, 'a@ferdmbp.local'}.
{node, b, 'b@ferdmbp.local'}.

{init, [a,b], [{node_start, [{monitor_master, true}]}]}.

{alias, demo, "./demo/"}.
{alias, meeting, "./meeting/"}.

{logdir, [all_nodes,master], "./logs/"}.

{suites, [b], meeting, all}.
{suites, [a], demo, all}.
{skip_cases, [a], demo, basic_SUITE, test2, "This test fails on purpose"}.

少しだけ変更しました。 ab という、テスト用に起動する必要ある2つのスレーブノードを定義しました。 特別なことは何もしていませんが、これでマスターが死んだ時に両方共が死ぬようになります。 ディレクトリのエイリアスはそのまま残っています。

logdir の値は興味深いですね。 all_nodesmaster というノードエイリアスは宣言しませんでしたが、ここには、ノードエイリアス名としてその2つが書いてあります。 all_nodes はCommon Testにおいてマスターでないノードすべてを表していて、 master はマスターノード自身を表しています。 すべてのノードを含めるには [all_nodes,master] と書く必要があるのです。 どうしてこのような名前になったかの説明は不要ですね。

../_images/venn-ref.png

上記のような値を設定した理由は、Common Testがスレーブノード毎にログ(とディレクトリ)を生成し、スレーブノードの各ログを参照するマスターログも生成するからです。 私には logs/ 以外のディレクトリは必要ありません。 スレーブノードのログは個々のスレーブノード上に保存されることに留意して下さい。 この場合、すべてのノードが同じファイルシステムを共有していない限り、マスターのログ内にあるHTMLリンクは動作せず、各ログを観るためには各ノードにアクセスするしなければならないでしょう。

最後にテストスイートと skip_cases エントリです。 これは先のspecファイルと同様ですが、各ノードに適用されます。 このようにして、特定のノード(ライブラリや依存するものが無いとわかっているノード)だけあるエントリを飛ばしたり、あるいはもっと突っ込んで言えばハードウェアがタスクを執行できないとわかっているエントリを飛ばすこともできます。

そのような分散テストを実行するためには、分散ノードを -name オプション付きで実行し、 ct_master を使ってテストスイートを実行しなければなりません:

$ erl -name ct
Erlang R15B (erts-5.9) [source] [64-bit] [smp:4:4] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.9  (abort with ^G)
(ct@ferdmbp.local)1> ct_master:run("dist.spec").
=== Master Logdir ===
/Users/ferd/code/self/learn-you-some-erlang/ct/logs
=== Master Logger process started ===
<0.46.0>
Node 'a@ferdmbp.local' started successfully with callback ct_slave
Node 'b@ferdmbp.local' started successfully with callback ct_slave
=== Cookie ===
'PMIYERCHJZNZGSRJPVRK'
=== Starting Tests ===
Tests starting on: ['b@ferdmbp.local','a@ferdmbp.local']
=== Test Info ===
Starting test(s) on 'b@ferdmbp.local'...
=== Test Info ===
Starting test(s) on 'a@ferdmbp.local'...
=== Test Info ===
Test(s) on node 'a@ferdmbp.local' finished.
=== Test Info ===
Test(s) on node 'b@ferdmbp.local' finished.
=== TEST RESULTS ===
a@ferdmbp.local_________________________finished_ok
b@ferdmbp.local_________________________finished_ok

=== Info ===
Updating log files
Updating /Users/ferd/code/self/learn-you-some-erlang/ct/logs/index.html... done
Updating /Users/ferd/code/self/learn-you-some-erlang/ct/logs/all_runs.html... done
Logs in /Users/ferd/code/self/learn-you-some-erlang/ct/logs refreshed!
=== Info ===
Refreshing logs in "/Users/ferd/code/self/learn-you-some-erlang/ct/logs"... ok
[{"dist.spec",ok}]

ct_run を使ってこのようなテストを実行する方法はありません。 Common Testはテストが実際に成功してもしなくても、すべての結果を ok とします。 この理由は ct_master がすべてのノードに接続できたかだけを表示するからです。 結果自体は実際は各ノードに保存されます。

Common Testはノードを起動して、どのようなCookieを使って起動したかも表示することも留意してください。 まずマスターを停止せずに再度テストを実行しようとすると、代わりに次の警告が表示されます:

WARNING: Node 'a@ferdmbp.local' is alive but has node_start option
WARNING: Node 'b@ferdmbp.local' is alive but has node_start option

これで大丈夫です。 これは、Common Testはリモートノードに接続できますが、ノードが既に生きているので、テストスペック内にあるinitタプルを呼び出すことはできないと分かった、という意味があるだけです。 Common Testがテストを実行するリモートノードを実際に起動する必要はないのですが、私は便利だと思います。

これは分散用のspecファイルの要点に過ぎません。 もちろん、より複雑なケース、たとえばより複雑なクラスタを設定したり、素晴らしい分散テストを書くこともできますが、テストが複雑になればなるほど、単純にテストが入り組んでいくほど、テスト自身も多くのエラーを抱えることになるため、自分のソフトウェアの性能を無事に証明できる自信がなくなっていきます。

../_images/bots.png

31.9. EUintをCommon Test内に統合する

あるときにはEUnitは実務において最適なツールで、またあるときにはCommon Testが最適であるため、一方を他方に含めることができれば望ましいでしょう。

Common TestのテストスイートをEUnitのそれに含めるのは難しい一方で、逆は非常に簡単です。 その理由は、 eunit:test(SomeModule) を呼び出した時には、関数はうまく動作していれば ok を返し、失敗したら error を返すという点にあります。

これはつまり、EUnitのテストをCommon Testのテストスイートに統合するためには、このような関数を作ればいいだけということになるからです:

run_eunit(_Config) ->
    ok = eunit:test(TestsToRun).

これで、 TestsToRun という説明書きで見つけることができるEUnitテストがすべて実行されます。 もし失敗したものがあれば、Common Testのログにその旨が表示され、出力を読んで何がおかしかったのか確認することができます。 かなり簡潔ですね。

31.10. さらにオプションはないの?

もちろんもっとオプションはあります。 Common Testはとても厄介な野獣なのです。 設定ファイルを変数に追加したり、テストの実行中のたくさんの箇所で実行されるフックを追加したり、テストスイート中でコールバックを使ったり、あるいはSSH、Telnet、SNMP、FTP越しにテストを行うモジュールを使うこともできます。

この章ではほんの上っ面を撫でただけですが、さらに深く使い込みたい場合にも良い足がかりになったと思います。 Common Testに関するより複雑なドキュメントに関しては、Erlang/OTPについてくる ユーザガイド を読んでください。 それだけでは読み進めるのは難しいですが、この章で説明した内容を理解することで、まちがいなくユーザガイドを理解する助けとなるでしょう。