Erlang用の軽量ユニットテストフレームワーク

原著者:Copyright 2004-2009 Mikael Remond, Richard Carlsson
原文:http://erlang.org/doc/apps/eunit/
原文更新:2009

EUnitはErlang用のユニットテストフレームワークです。とてもパワフルで柔軟かつ、使用するのが簡単であり、構文上のオーバーヘッド(書かなければいけないもの)が少ないのが特徴です。

EUnitは、ケント・ベックと、エリック・ガンマが作成したJUnit(と、ケント・ベックがそれ以前に作成した、Smalltalk用のSUnit)を起源とする、オブジェクト指向言語用のユニットテストフレームワーク群から来たアイディアを元に開発されています。しかし、EUnitは、より関数型プログラミングと、並列プログラミングに適応するような技術が使用されています。また、関連するフレームワーク群よりも、言葉数が少なくて済むことが多いです。

EUnitは多くのプリプロセッサマクロを使用しますが、利用者にできるだけ負担をかけないように設計されています。また、既存のコードと衝突を起こさないように設計されています。モジュールにEUnitのテストを追加する際には、通常は既存のコードに変更を加える必要がありません。これに加え、モジュールのエクスポートされた関数をテストするだけであれば、いつでも、完全に分割したモジュールにテストをおくことができます。何らかの衝突が起きることはありません。

1.1 ユニットテスト

ユニットテストは、お互いに分離されている「ユニット」という単位で個別にテストを行う方法です。ユニットは特定のサイズにしなければならない、ということはありません。ユニットが一つの関数であることもあるし、モジュールを示すこともあります。またプロセスはアプリケーション全体を一つのユニットと呼ぶこともあります。が、多くのケースにおいては、テストを行うユニットは、複数の関数、もしくはモジュールであることが多いです。ユニットをテストするためには、個別のテストをいくつか用意したり、これらのテストを実行するのに必要な最小限の環境を作るためのセットアップ(通常、設定が必要でないケースがほとんどですが)を行ったり、テストを実行して結果を収集したり、後でテストを再実行するために最後に必要となる片付けを行ったりします。ユニットテストのフレームワークを使用することで、これらのプロセスのそれぞれの段階の作業を軽減することができて、テストを書くのがラクになります。そしてテストを実行するのも簡単になり、どのテストが失敗したかが簡単に確認できるようになり、バグを修正することができるようになります。

1.1.1 ユニットテストのメリット

プログラム変更のリスクを削減する

ほとんどのプログラムは、作成されてから破棄されるまでの間に変更されるでしょう。バグがあれば修正され、機能が追加され、必要になれば最適化が行われ、また、リファクタリングやコードの整理が必要となれば、同じ機能を達成するやり方の別の方法に修正されます。しかし、動作しているプログラムに対して、これらの変更を加えることは、新しいバグや、既に修正済みのバグを再び入れ込んでしまうリスクを常に孕んでいます。少ない手間ですぐに実行できるユニットテスト群があれば、簡単にコードが期待通りに動作するかどうかを知ることができます。このようなテストをリグレッションテストあるいは、回帰テスト(用語集を参照)と呼ばれます。これは、コードの変更や、リファクタリングをする抵抗を減らすのに役に立ちます。

開発プロセスの遵守とスピードアップ

テストをパスするコードを開発するのにフォーカスするため、プログラマはより生産的になります。仕様以上に作り込んでしまうこともなくなりますし、不必要な最適化をしてしまうことも防げます。最初から正しいコードを作成することができるようになります。これはテスト駆動開発と呼ばれます(用語集を参照)。

インタフェースと実装の分離に役立つ

テストを書いている時に、テストの実行とは関係のない、そこにあるべきではない依存関係があるのを見つけるかもしれません。よりクリーンな設計をするためにこのような依存関係は取り除く必要があります。早めにこのような依存関係を切っていくことで、コード中に悪い依存関係が広がっていくのを未然に防ぐことができます。

部品の統合を簡単に

ボトムアップでテストをしていくことで、小さいプログラムのユニットから開発を開始することができ、期待通りに動作するという信頼性も生まれます。これにより、テスト済みのユニットを組み合わせて作る、上位のレベルの部品が仕様通りに動作するかのテストも簡単になります。これは統合テスト(用語集参照)と呼ばれます。

ドキュメントになる

テストはドキュメントとしても読むことができます。テストを見れば、正しい使用法と、正しくない使用方法、そして期待される結果が分かります。

1.2 用語集

ユニットテスト

プログラムユニットが、仕様に従って、期待通りに動作するかどうかのテストになります。ユニットテストは、リグレッションテストを書く場合にも重要な構成要素となります。リグレッションテストというのは、何らかの理由でコードが変更された場合に、仕様通りの動作を保っているかどうかをチェックするものになります。

リグレッションテスト(回帰テスト)

プログラムに変更を加えたあとにテストを実行し、以前と同じようにプログラムが動作するかどうかチェックすることです。もちろん、意図して変更した振る舞いは除きます。ユニットテストはリグレッションテストとして重要ですが、リグレッションテストは単にユニットテストの範疇に収まるものではありません。バグや互換性など、テストの振る舞いは通常の仕様とは異なるものです。

統合テスト

個別に開発されたプログラムのユニット(それぞれのユニットは個別にユニットテストされているものとします)を統合したときに、期待通りに動作するかどうかをテストすることを指します。システムの開発された方法により、統合テストは単に「階層の違うユニットテスト」と同等の場合もありますが、他の種類のテストも含みます。システムテストとの違いを確認してください。

システムテスト

すべてのユニットを組み合わせた完全なシステムが、仕様通りに動作するかどうか検証します。具体的には、システムテストでは実装の詳細の知識が要求されるようなものであってはいけません。システムテストには、基本的な機能性とは別に、様々な異なる側面から見たシステムの振る舞いに関するテストを含みます。例えば、パフォーマンスや、ユーザビリティ、信頼性などになります。

テスト駆動開発

テストを実装コードよりも先に書いて、その後にテストをパスする最小の実装を行うことによりソフトウェアを開発していくという、プログラム開発テクニックです。この方法を使うと、正しい問題を解決することにのみフォーカスします。そして、ユニットテストによって、そのプログラムが完了した後の姿を決定することで、必要以上に実装が複雑になることを防ぎます。もしテストをパスして仕様を満足したあとは、それ以上機能を追加する必要はないのですから。

モックオブジェクト

他のユニットB(引数として渡されるか、参照される)と何らかの連携を行う必要のある、ユニットA(例えば関数)をテストする必要があり、なおかつユニットBの実装が完了していない、というケースが時々あります。この場合に、ユニットAをテストするために、本物のBと同じような振る舞いをするニセ者を代わりに利用することがあります。これを「モックオブジェクト」と呼びます。もちろん、これは本物のユニットBを実装する方が、モックオブジェクトを作るよりも大変である場合にのみ役立ちます。

テストケース

明確に定義され、識別可能なテストの最小単位です。実行されると、テストケースはパスするか、失敗するかのどちらかになります。テストのレポートでは、どのテストケースが失敗したか明確に区別されなければなりません。

テストスイート

テストケースを集めたものになります。一般的には、とある関数や、モジュール、サブシステムなど、共通のターゲットに対するテストを集めたものになります。テストスイートは、他の小さなテストスイートを含んだ、再帰的な構造として作成することもできます。

1.3 テストをはじめよう

1.3.1 EUnitヘッダファイルのインクルード

EUnitをErlangのモジュールで使用する、一番簡単な方法は、モジュールの先頭(-module宣言の直後で、他の関数宣言の前)に以下の行を追加するやり方になります。

-include_lib("eunit/include/eunit.hrl").

この行を追加すると以下のような効果が現れます。

  • エクスポートされるtest()関数が作成されます。ただし、テスト機能がオフになっていたり、モジュール内に既にtest()関数が含まれている場合を除きます。この関数は、モジュール内に定義されているユニットテストをすべて実行するのに使用することができます。
  • _test()、もしくは_test_()という名前にマッチするすべての関数をモジュールから外にエクスポートします。これも、テスト機能がオンになっているか、EUNIT_NOAUTOマクロが定義されていない場合のみ動作します。
  • テストを書くのを手助けするすべてのEUnitのプリプロセッサマクロを使用可能にします。

Note: -include_lib(...)が実行可能になるためには、Erlangのモジュールサーチパスにはeunit/ebinで終わる名前を持つディレクトリ(EUnitインストールディレクトリのebinサブディレクトリ)が含まれる必要があります。もし EUnitが lib/eunitのような、Erlang/OTPのシステムディレクトリにインストールされている場合には、ebinサブディレクトリはErlangの起動時に自動的にサーチパスに追加されます。そうでない場合には、erlコマンド、もしくはerlcコマンドに-paフラグを渡して、サーチパスにきちんと追加する必要があります。例えば、.erlファイルをコンパイルするアクションがMakeファイルに書かれていたとすると、以下のように書きます。

erlc -pa "path/to/eunit/ebin" $(ERL_COMPILE_FLAGS) -o$(EBIN) $< 

あるいは、Erlangをインタラクティブに実行する場合に、常にEUnitを有効にしたい時には、$HOME/.erlangファイルに以下の行を追加します。

code:add_path("/path/to/eunit/ebin").

1.3.2 シンプルなテスト関数を書く

EUnitフレームワークはErlangでユニットテストを書くのをとても簡単にしてくれます。テストを書く方法は何通りかありますが、まずは一番簡単なやり方から開始します。

EUnitは、名前の最後が_test()で終わる、引数のない関数をシンプルなテスト関数であると認識します。任意の値(EUnitが破棄してしまいますが)を返すとテストは成功とみなされます。また、なんらかの例外を投げる(ただし中断しない)場合には失敗したとみなされます。実行が中断すると、テストの実行自体が途中終了します。

以下のコードが、シンプルなテスト関数の例になります。

reverse_test() -> lists:reverse([1,2,3]).

このテストは、[1, 2, 3]というリストに対して、lists:reverse(List)を実行するとクラッシュはしない、というテストになります。これはすばらしいテストとは言えませんが、多くの人がこのようなシンプルな関数を書き、自分のコードの基本的な機能性をテストしています。これらのテストはコードを変更しなくても、名前がマッチすればEUnitから直接使用されます。

失敗を表すためには例外を使用します。そうすることで、より詳しいテストを書くことができます。もし期待していない結果が得られた場合には、クラッシュさせて例外を投げる必要があります。一番簡単な方法は = を使ったパターンマッチを使用する方法です。

reverse_nil_test() -> [] = lists:reverse([]).
   reverse_one_test() -> [1] = lists:reverse([1]).
   reverse_two_test() -> [2,1] = lists:reverse([1,2]).

もし lists:reverse/1 に何かバグがあり、[1, 2]という入力に対して、 [2, 1]以外の結果を返した場合には、上記の最後のテストはbadmatch errorを投げます。最初の二つ(badmatchが得られなかったとすると)はそれぞれ単純に [] と [1] を返し、成功したことになります。EUnitは魔法使いではないので、比較値を書かない場合には、値を返すテストを書いて、もしそれが間違った値だったとしても、EUnitは成功したとみなします。もし返り値が仕様通りでない値を返した場合にはクラッシュするようなテストを書く必要があります。

assertマクロの使用: もしBoolean演算をテストで使用したい場合には、assertマクロを使うのが手軽です(詳しくはEUnitマクロを参照)。

length_test() -> ?assert(length([1,2,3]) =:= 3).

?assert(Expression) というマクロは式を評価します。そして評価した結果がtrueでない場合には、例外を投げます。そうでない場合にはokを返します。上記の例では、 lengthを呼び出した結果が3を返さなかった場合に失敗します。

1.3.3 EUnitの実行

もし前の説明に書いたように -include_lib("eunit/include/eunit.hrl") 宣言をモジュールに付けた時には、モジュールをコンパイルするだけで大丈夫です。自動的にエクスポートされた test() 関数を実行します。例えば、m という名前のモジュールを作ったとしたら、 m:test() がEUnitを実行し、モジュールの中で定義されたすべてのテストを実行します。テストの関数に対して、 -export 宣言を付ける必要はありません。魔法によって、これらはすべて自動的に行われます。

eunit:test/1関数を使用することで、各々のテストを実行することもできます。例えば、より進んだテストデスクリプタ( EUnitテスト表現を参照)を使用したい場合などです。例えば、eunit:test(m) を実行するのは、自動生成された m:test()を実行するとの同等です。また、 eunit:test({inparallel, m}) を実行すると、同じテストケースを実行しますが、それぞれのテストは並列で実行されます。

テストを別のモジュールに分ける

通常のコードと、エクスポートされた関数に対するテストコードを分けたいとします。このような場合には、もしモジュールの名前がmであったとすると、テスト関数をm_tests(m_testではなく複数形)というモジュールの中に書くことができます。EUnitにモジュールmをテストするように指示した場合、m_testsというモジュールを探しにいって、この中に定義されたテストが実行されるようになります。モジュール名に関してはプリミティブというところで詳しく説明します。

EUnitにより標準出力を取得する

もしテストコードが標準出力に何かを書き出したとすると、テストが実行されたときにコンソールに何もテキストが表示されないのを見て驚くでしょう。これはEUnitがテスト関数から出力される、すべての標準出力の結果をキャプチャーしているからです。これにはセットアップ、クリーンナップの関数も含まれますが、ジェネレータ関数は含まれません。そして、この結果は、エラーが発生した場合に、テストレポートの中に出力されます。テストの実行中にEUnitを迂回して、テキストを直接コンソールに書き出したい場合には、io:format(user, "~w", [Term])というユーザ出力ストリームを利用することができます。推奨される方法としては、EUnitのデバッグマクロを使用することで、シンプルに書くことができます。

1.3.4 テストを生成する関数を書く

シンプルなテストを書く際に障害となるのは、テストケースごとに別々の名前を持った、個別の関数を書く必要があるということです。テストを書くためのコンパクトで、より柔軟な方法は、テストそのものを書くのではなく、テストを返す関数を書くことです。

EUnitは、_test_()(最後のアンダースコアが大切)という名前で終了する関数を、テストジェネレータ関数として扱います。テストジェネレータは、EUnitで実行することができるテスト群の表現を返します。

データとしてテストの表現

基本的なテスト表現は、テストは引数のない1つのfun式で表されます。例えば、以下のようなテストジェネレータを作成することができます。

basic_test_() ->
       fun () -> ?assert(1 + 1 =:= 2) end.

このジェネレータは、以下のようなシンプルなテストと同じ結果になります。

simple_test() ->
    ?assert(1 + 1 =:= 2).

実際には、EUnitは内部ではすべてのシンプルなテストをfun式として扱います。これらの式をリストの中に格納して、一つずつ実行します。

テストを書くためのマクロを使用する

テストをよりコンパクトで読みやすく書くためには_testマクロ(先頭のアンダースコアを忘れずに)を使用することができます。このマクロは、テストが書かれているソースコード内の行番号の情報を自動的に付加し、タイプ数を節約できます。

basic_test_() ->
    ?_test(?assert(1 + 1 =:= 2)).

_testマクロは引数としてあらゆる式(テスト本体)を受け取り、それを追加の情報とともにfun式の内部に格納した状態で展開されます。本体は、シンプルなテスト内に書くように、テストを表す式なら何でも指定することができます。

テストオブジェクトを作成するアンダースコアが前に付いたマクロ

上記の例はもっと短くすることができます。マクロ名の前にアンダースコアがついたassertマクロ群などのほとんどのテストのためのマクロは、自動的に?_test(...)ラッパーを付加します。上記の例はもっとシンプルに以下のように書くことができます。

basic_test_() ->
    ?_assert(1 + 1 =:= 2).

_assertマクロを、assertマクロの代わりに使用することで、まったく同じ文となります。最初にアンダースコアがついたマクロはテストオブジェクトを表す記号として見ても差し支えありません。

1.3.5 サンプル

1つのサンプルは百聞にしかず。以下の小さなErlangモジュールはEUnitを実践の中でどのように使用できるのかということを示しています。

-module(fib).
   -export([fib/1]).
   -include_lib("eunit/include/eunit.hrl").

   fib(0) -> 1;
   fib(1) -> 1;
   fib(N) when N > 1 -> fib(N-1) + fib(N-2).

   fib_test_() ->
       [?_assert(fib(0) =:= 1),
        ?_assert(fib(1) =:= 1),
        ?_assert(fib(2) =:= 2),
        ?_assert(fib(3) =:= 3),
        ?_assert(fib(4) =:= 5),
        ?_assert(fib(5) =:= 8),
        ?_assertException(error, function_clause, fib(-1)),
        ?_assert(fib(31) =:= 2178309)
       ].

著者のメモ: 初めてこのサンプルを書いた際に、fib関数の中の+演算子の代わりに、間違って*演算子を書いてしまいました。もちろん、テストを実行したらすぐに間違ったことが分かりました。

EUnitでテストを定義するすべての方法を表したリストを見るためには、EUnitテスト表現を見てください。

1.3.6 テストの無効化

コンパイル時にNOTESTマクロを定義することでテストを除外することができます。erlcのオプションとして指定するには以下のように書きます。

erlc -DNOTEST my_module.erl

コード中に書く際は、EUnitヘッダファイルをインクルードするよりも前に、以下のマクロ定義を書きます。

-define(NOTEST, 1).

定義する値は重要ではありませんが、1かtrueを良く使用します。EUNIT_NOAUTOマクロが定義されない場合には、明示的にエクスポートされているものを除いて、すべてのテスト関数を自動的に削除し、テストを無効にします。

実際にEUnitをアプリケーションの中に実際に使用するが、デフォルトではテスト機能をオフにしたい場合には以下のようにヘッダファイルに書きます。

-define(NOTEST, true).
-include_lib("eunit/include/eunit.hrl").

このヘッダファイルがすべてのモジュールにインクルードされているものとします。一カ所だけを修正することで、テストのデフォルト設定を変更することができるようになります。また、NOTESTの定義はコードを修正しなくても上書きできるようになっています。一時的にテストを有効にするためには、TESTをコンパイラオプションとして定義します。

erlc -DTEST my_module.erl

これらのマクロの詳細については、コンパイル制御マクロを参照してください。

1.3.7 コンパイル時のEUnitへの依存を回避する

もし、自分で作ったソースコードを配布して、他のユーザが自分のアプリケーションに組み込み、コンパイルして実行しようとしているとしたら、EUnitが手に入らなかったとしてもコードがコンパイルできるようにしたいと思いますよね?前の方のセクションで出てきたサンプルを参考に、共通ヘッダファイルに以下のようなコードを書くことができます。

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.

もちろん、EUnitのマクロを使用する、すべてのテストコードは、-ifdef(TEST)、あるいは、-ifdef(EUNIT) セクションの中に書くようにします。

1.4 EUnitマクロ

EUnitのすべての機能はプリプロセッサマクロを使用しなくても使用できますが、EUnitのヘッダファイルには、ユニットテストを書くのを簡単にするためのマクロが数多く定義されています。これを使用すると、詳細まで書かなくても、可能な限り短いコードになります。

明確に定義されているものを除いて、EUnitのマクロを使用しても、EUnitのライブラリコードへの実行時の依存関係は発生しません。これは、コンパイル時にテストが有効になっているか、無効になっているか、というのには依存しません。

1.4.1 基本マクロ

_test(Expr)

式をテストオブジェクトに変換します。テストオブジェクトはfun式でラップされ、ソースファイルの行番号が付加されます。技術的には、{?LINE, fun () -> (Expr) end}と書くのと同一です。

1.4.2 コンパイル制御マクロ

EUNIT

このマクロはコンパイル時にEUnitが有効な時にはいつでも定義されています。このマクロは、以下のようにテストコードを条件コンパイル内に置くときに使用します。

-ifdef(EUNIT).
    % ここにテストコードを書く
    ...
-endif.

例えばテストが無効になっているときには、EUnitヘッダファイルをインクルードしないでコンパイルされることもあります。TEST, NOTESTの項目も参照してください。

EUNIT_NOAUTO

このマクロが定義されると、自動的にテスト関数をエクスポートする機能が無効になります。

TEST

このマクロは、コンパイル時にEUnitが有効になっている場合にはいつも定義されます。ユーザが自分で別の値を定義した場合を除いて、trueとして定義されます。このマクロはテストコードを条件コンパイルする際に使用することができます。NOTEST, EUNITの項目も参照してください。

EUnitに依存しているテストコードについて条件コンパイルする場合には、EUNITマクロを使用する方が望ましいです。TESTマクロはもっと一般的なテスト条件に使用されます。この場合はTESTマクロの方が望ましいでしょう。

TESTマクロは、TOTESTマクロを上書きすることができます。NOTESTが定義されていたとしても、EUnitヘッダファイルがインクルードされる前にTESTが定義されていれば、EUnitは有効な状態でコンパイルされます。

NOTEST

このマクロは、EUnitがコンパイル時に無効になっている場合にはいつも定義されます。ユーザが自分で別の値を定義した場合を除き、trueとして定義されます。TESTマクロと比較してみてください。

このマクロは条件コンパイルにも使用することができますが、一般的にはテストを無効化用途に使用します。EUnitヘッダファイルをインクルードする前にNOTESTが定義されていて、なおかつTESTが定義されていない場合には、EUnitが無効の状態でコンパイルされます。テストの無効化も参照してください。

NOASSERT

このマクロは、テストが無効になっていて、assertマクロを使用しても無効になる場合に定義されます。テストが有効な場合には、このマクロは常に自動的に有効になります。無効化することはできません。assertマクロの項も参照してください。

ASSERT

もしこのマクロが定義されていると、NOASSERTマクロを上書きします。他の設定によらず、常にassertマクロを有効にすることができます。

NODEBUG

もしマクロが定義されていれば、デバッグマクロが動作しなくなります。詳しくはデバッグマクロの説明を見てください。NODEBUGを定義するとテストが定義されていたとしても、NOASSERTが定義されます。

DEBUG

もしこのマクロが定義されると、NODEBUGマクロの動作が上書きされます。デバッグ用のマクロを強制的に使用可能にします。

1.4.3 ユーティリティマクロ

テストをより短く、読みやすくすることができます。

LET(Var,Arg,Expr)

ローカルな、Expr内で、Var = Argというローカルな束縛を作成することができます。 (fun(Var)->(Expr)end)(Arg)と定義するのと同じです。この束縛はExprの外にはエクスポートされません。このVarに対する束縛は、使用されるスコープの他の束縛を邪魔することはありません。

IF(Cond,TrueCase,FalseCase)

もしCond式がtrueの場合にはTrueCaseが評価されます。そうでない場合はFalseCaseが評価されます。これは (case (Cond) of true->(TrueCase); false->(FalseCase) end).と同等です。Condはブーリアンの値でない場合にはエラーになります。

1.4.4 assertマクロ

?_assert(BoolExpr)のような、"_"アンダースコアの文字から始まる名前のマクロも存在します。この形式のマクロは、テストを実行するだけでなく、"テストオブジェクト"も作成します。これは?_test(assert(BoolExpr))のように書くのと同じ動作をします。

もし、EUnitヘッダファイルがインクルードされる前にNOASSERTマクロが定義されると、テストも無効化されている時に、これらのマクロも空の実装に取り替えられます。詳細については、コンパイル制御マクロを参照してください。

assert(BoolExpr)

テストが有効になっている場合に、BoolExpr式の評価を行います。結果がtrueでなければ、それを知らせる例外が生成されます。例外発生しなかった場合には、マクロ式の評価結果は、okというアトムになります。BoolExprの値は返されません。もしテストが無効な場合には、okアトムを返すコード以外は生成されません。BoolExprも評価されなくなります。

一番使用される方法は以下のようになります。

?assert(f(X, Y) =:= [])

このassertマクロは、ユニットテストの中だけでなく、どこでも使用することができます。以下のように、DbCで行う、事前条件/事後条件/不変条件のチェックにも使用することができます。

some_recursive_function(X, Y, Z) ->
    ?assert(X + Y > Z),
    ...
assertNot(BoolExpr)

assert(not (BoolExpr))と同等です。

assertMatch(GuardedPattern, Expr)

テストが有効になっている時に、Exprを評価し、結果がGuardedPatternに対して、マッチするかどうか検証します。もしパターンにマッチしなかった場合には、それを知らせる例外を生成します。このあたりの詳細はassertマクロの説明を見てください。GuardedPatternはcase構文の -> シンボルの左側に書くことができるものであれば、なんでも書くことができます。ただし、カンマで区切られたガードテストを含むものを除きます。

シンプルなマッチのテストでも、= ではなくて、assertMatchを使用すると、より詳細なエラーメッセージを生成することができます。

サンプル:

?assertMatch({found, {fred, _}}, lookup(bloggs, Table))
?assertMatch([X|_] when X > 0, binary_to_list(B))
assertEqual(Expect, Expr)

テストが有効な場合に、ExpectとExprの両方の式を評価して、結果が等しいかどうか比較します。もし等しくなかった場合には、情報を伝える例外を生成します。詳しくはassertマクロの説明を見てください。

assertEqualは、左側の期待値がパターンではなくて、計算後の値である場合には、assertMatchよりも良いでしょう。また、?assert(Expect =:= Expr)よりも、詳しい説明が得られます。

サンプル:

?assertEqual("b" ++ "a", lists:reverse("ab"))
?assertEqual(foo(X), bar(Y))
assertException(ClassPattern, TermPattern, Expr)
assertError(TermPattern, Expr)
assertExit(TermPattern, Expr)
assertThrow(TermPattern, Expr)

Exprを評価して、その中で発生した例外をキャッチし、それが期待されたClassPattern, TermPatternと一致しているか検証します。マッチしないか、Exprを評価した中で例外が発生しなかった場合には、情報を伝える例外を生成します。詳しくはassertマクロの説明を参照してください。assertError, assertExit, assertThrowは、ClassPatternとしてerror, exit, throwを設定してassertExceptionを使用した場合と、それぞれ同一です。

サンプル:

?assertError(badarith, X/0)
?assertExit(normal, exit(normal))
?assertException(throw, {not_found,_}, throw({not_found,42}))

1.4.5 外部コマンド実行用マクロ

外部コマンドはOSに強く依存します。これらのマクロ以外にも、現在のOSに依存したテストを作成するために、標準ライブラリのos:type()をテストジェネレータ関数の中で使用することもできます。

注: これらのマクロはテストが有効な状態でコンパイルされると、EUnitのライブラリコードへの実行時の依存が発生します。

assertCmd(CommandString)

テストが有効な場合に、CommandStringを外部コマンドとして実行します。もし外部プログラムの返値がゼロでなかった場合には、それを通知する例外が生成されます。例外が発生しなかった場合には、マクロの式の結果は、okアトムになります。もしテストが無効化されている場合には、okアトムを返す以外のコードは生成されず、外部コマンドも実行されます。

一般的な使用法:

?assertCmd("mkdir foo")
assertCmdStatus(N, CommandString)

assertCmd(CommandString)マクロと同様ですが、もしステータス値としてN以外の値が返されると例外を生成します。

assertCmdOutput(Text, CommandString)

テストが有効な時に、CommandStringを外部コマンドとして実行します。コマンドが出力したものが、指定した文字列のTextと一致しなかった場合には、違いの情報を伝える例外を出力します。どのようなプラットフォームでも、改行コードはLFに正規化されます。もし例外が発生しなかった場合には、このマクロはokアトムを返します。

cmd(CommandString)

CommandStringを外部文字列として実行します。返り値が0(成功を意味します)でなかった場合には、情報を伝える例外が生成されます。成功した場合には、このマクロは、コマンドが出力した物をフラットな文字列として返します。どのようなプラットフォームでも、改行コードはLFに正規化されます。

このマクロは、エラーが発生しても、テストをきちんと実行するために、ファイルの作成と削除、OS特有のタスクの実行などを、フィックスチャのセットアップ、片付けのセクションで使用するのが便利です。

UNIX環境で動作するサンプル:

{setup,
 fun () -> ?cmd("mktemp") end,
 fun (FileName) -> ?cmd("rm " ++ FileName) end,
 ...}

1.4.6 デバッグマクロ

EUnitではいくつかのデバッグを補助するマクロを定義しています。これらを使うと、標準出力ではなく、直接コンソールにメッセージをひょうじすることができます。これらのマクロは同じ基本フォーマットを使用して出力されます。これにはデバッグマクロが実行されたファイル名、行番号が含まれ、開発環境(Emacsのバッファ内でErlangを実行している場合など)によっては、出力メッセージから、ソースコードの該当する行に直接ジャンプすることができます。

EUnitヘッダファイルがインクルードされる前にNODEBUGマクロが定義されると、これらのマクロは何もしなくなります。詳しくはコンパイル制御マクロの項目を見てください。

debugHere

コンソールに現在のファイルと行番号を表すマーカーを表示するだけのマクロです。このマクロは引数を取りません。結果は常にokとなります。

debugMsg(Text)

Textをメッセージとして出力します。Textはプレーンな文字列、IOリスト、アトムが使用可能です。実行結果は常にokとなります。

debugFmt(FmtString, Args)

このマクロは、テキストをio:format(FmtString, Args)と同様にテキストをフォーマットして、debugMsgと同じように出力します。結果は常にokとなります。

debugVal(Expr)

Exprに対するソースコードと、現在の値の両方を出力します。例えば、?debugVal(f(X))は"f(X) = 42"と表示されます。(ほとんどの単語は切り詰めて表示されます)。評価結果はExprの値となります。そのため、デバッグ機能を有効にしてコンパイルされている時には、このマクロで式(式が使える所はどこでも)をラップして、コードがコンパイルされた時の値を表示するという使い方ができます。

debugTime(Text,Expr)

Exprを評価したときの時間とテキストを表示します。評価結果はExprの値となります。そのため、デバッグ機能を有効にしてコンパイルされている時には、このマクロで式(式が使える所はどこでも)をラップして使用することができます。例えば、List1 = ?debugTime("sorting", lists:sort(List))を実行すると、"sorting: 0.015 s"と表示されます。

1.5 EUnitテスト表現

EUnitでテストやテストセットをデータとして表現する方法は柔軟でパワフルで、簡潔です。このセクションでは、詳細の表現について説明していきます。

1.5.1 シンプルテストオブジェクト

シンプルテストオブジェクトは以下の表現のうちの1つになります。

  • 空のファンクションの値(例えば、引数を一つもとらないfunなど)。サンプル:
    fun() -> ... end
    fun some_function/0
    fun some_module:some_function/0
  • ModuleName:FunctionName/0 という関数を参照する、{ModuleName, FunctionName}というアトムのペア。
  • {LineNumber, SimpleTest}のペア。LineNumberは負でない整数で、SimpleTestは他のシンプルテストオブジェクト。LineNumberはテストのソースコード行数を示す。このようなペアは普段は、?_test(...)マクロを使って生成される。 詳しくは基本マクロを参照してください。

まとめると、シンプルテストオブジェクトは、引数をもたない関数を一つだけ持ちます。また、行番号などの追加のメタデータをいくつか持ちます。テストの評価は、この関数が成功するか、失敗するかで行われます。成功した場合には何かの値を返します。ただし、この値は無視されます。例外が投げられると失敗と判定されます。

1.5.2 テストセットと、深いリスト

テストオブジェクト群をリストに入れるだけでテストセットを作成することができます。T_1, ..., T_Nというのが個別のテストオブジェクトだったとすると、[T_1, ..., T_N]はこれらのオブジェクトで構成されるテストということになります。順番もこの通りに扱われます。

同様の方法でテストセットをつなげることができます。S_1, ..., S_Kがテストセットだったとすると、[S_1, ..., S_K]もまたテストセットになります。テストの順番は、S_iに含まれるテストは、S_(i+1)のテストの前となるようにテストセットごとに配置されます。

よく使用されるテストセット表現は深いリストになります。シンプルなテストオブジェクトは、一つだけのテストを含んでいるテストセットとして見えます。Tと[T]に違いはありません。

モジュールもテストセットの表現として利用されます。プリミティブのModuleNameの項目も参照してください。

1.5.3 タイトル

すべてのテストとテストセットにはタイトルを注釈でつけることができます。テストもしくはテストセットをTとすると、注釈を付けるには{タイトル、T}というペアのタプルを作ります。タイトルは文字列です。あらゆるテストは通常タプルを使用して、タイトル文字列を最初の要素として、{"タイトル", ...}という形式で作成するだけで表現できます。さらに{"タイトル", {...}}というような、余計なタプルを付ける必要はありません。

1.5.4 プリミティブ

以下の要素がプリミティブになります。他のテストセットを引数に含むことはできません。

ModuleName::atom()

モジュール名を表す単独のアトム。これは{モジュール, モジュール名}と同等です。これはeunit:test(モジュール)という形式でコールされたときに良く使用されます。

{module, ModuleName::atom()}

これは与えられた名前のモジュール内の、エクスポートされたテスト関数からなるテストセットで構成されます。これらの関数は、引数がゼロで、名前の末尾が_testもしくは_test_で終わるものになります。..._test()という関数は、シンプルなテストで、..._test_()という関数はジェネレータになります。

これに加えて、EUnitは、ModuleNameというモジュールに_testsという末尾の文字列の追加された別のモジュールも探しに行きます。もし見つかれば、このモジュールに含まれるテストもすべて追加されます。ただし、ModuleNameにすでに_testsという文字が後ろについている場合は探しに行きません。 {module, mymodule}という仕様は、mymoduleと、mymodule_testsというモジュールに含まれる、すべてのテストを実行することになります。通常、_testsモジュールは、メインのモジュールの公開インタフェースに対するテストケースのみを含み、他のコードは含まないようにすべきです。

{application, AppName::atom(), Info::list()}

これは、.appファイルの中で見られる、通常のErlang/OTPアプリケーションのディスクプリタになります。結果となるテストセットはInfoに含まれるモジュールの中のエントリーの中のモジュールから構成されます。

{application, AppName::atom()}

これは、特定のアプリケーションに含まれるすべてのモジュールにあるテストセットを作成します。アプリケーションの.appファイル({file, FileName}参照)を調査するか、もしくは存在しない場合には、アプリケーションのebinディレクトリ({dir, Path}を参照)に含まれるすべてのオブジェクトファイルを調べます。もしそれでも見つけられない場兄は、code:lib_dir(AppName)ディレクトリが使用されます。

Path::string()

ファイルやパスを表す文字列です。これは{file, Path}もしくは{dir, Path}と同等になります。このPathは、それぞれファイルシステムの中で参照されます。

{file, FileName::string()}

もし、FileNameの末尾が、オブジェクトファイル(.beam)を表す拡張子がついている場合には、EUnitはそのファイルをリロードして、テストしようとします。そうでない場合には、ファイルを、テスト仕様を含むテキストファイルとみなして、標準ライブラリ関数のfile:path_consult/2を使用して読み込みます。

ファイル名が絶対パスでない場合には、まず現在のディレクトリからの相対パスとして検索します。その後は通常のサーチパス(code:get_path())を利用します。これにより、よく使う"app"ファイル名はパスを指定しなくても直接指定することが可能になります。例えば、"mnesia.app"のように書くことができます。

{dir, Path::string()}

これは、指定されたディレクトリの中のすべてのオブジェクトファイルをテストします。{file, FileName}を使用してすべてのファイルを個別に指定するのと同じように動作します。

{generator, GenFun::(() -> Tests)}

ジェネレータ関数であるGenFunがコールされ、テストセットが生成されます。

{generator, ModuleName::atom(), FunctionName::atom()}

テストセットを生成するのに、ModuleName:FunctionName()が呼ばれます。

{with, X::any(), [AbstractTestFun::((any()) -> any())]}

Xの値を、リストに含まれている単体の関数に適用し、それぞれを引数のないテスト関数に変換します。AbstractTestFunは通常のテスト用のfunとほぼ同じですが、引数をゼロ個ではなく、一つだけ受け取ります。パラメータを受け取る前の、正式なテスト関数になる前のテストは、いくつかの情報が足りない状態になります。実際、 {with, X, [F_1, ..., F_N]}は、[fun () -> F_1(X) end, ..., fun () -> F_N(X) end].と同等になります。これは特に、抽象的なテスト関数がすでにきちんとした関数として定義されている場合に便利です。{with, FD, [fun filetest_a/1, fun filetest_b/1, fun filetest_c/1]}は、[fun () -> filetest_a(FD) end, fun () -> filetest_b(FD) end, fun () -> filetest_c(FD) end]と一緒ですが、よりコードが短くなります。フィックスチャの項目もご覧ください。

1.5.5 制御

これから説明していく表現を使うと、どこでどのようにテストが実行されるかを制御することができます。

{spawn, Tests}

それぞれのテストをサブプロセスに分けてテストを行います。現在のテストプロセスはそれぞれのテストが終了するのを待ちます。これはプロセスの状態を独立した、新規のプロセスとしてテストを実行するのに役立ちます。EUnitは常に少なくとも一つのサブプロセスを自動でスタートさせます。呼び出しもとのプロセスでテストが実行されることはありません。

{spawn, Node::atom(), Tests}

{spawn, Tests}とほぼ同一ですが、テストを、指定されたErlangのノードで実行します。

{timeout, Time::number(), Tests}

指定されたタイムアウト時間の中でテストを実行します。Timeは秒で設定します。60を指定すると1分に、0.1を指定すると1/10秒になります。もしタイムアウトすると、終了していないテストは強制的に中断されます。設定されたタイムアウトの時間は、セットアップと片付けも含んだ、フィックスチャ全体の時間になります。もしタイムアウトが起動すると、片付けのコードが実行されないで、突然強制終了されることになります。

{inorder, Tests}

指定されたテストを、厳格な順序で実行します。{inparallel, Tests}.も参照してください。デフォルトでは、テストはinorderもinparallelも指定されていません。テストのフレームワークまかせの方法で実行されます。

{inparallel, Tests}

可能であれば、指定されたテストを並列で実行します。{inorder, Tests}も参照してください。

{inparallel, N::integer(), Tests}

{inparallel, Tests}と同様ですが、平行実行の数をN以下に設定します。

1.5.6 フィックスチャ

フィックスチャは、あるテストセットを実行するのに必要な状態を表します。EUnitはテストセットのためのローカルの状態を簡単に設定しやすいような機能を提供しています。また、テストの結果(success, failures, timeoutsなど)に関わらず、テストセットが終了したときに自動的にクリーンアップを行うこともできます。

説明を簡単にするために、まず最初に定義をリストアップします。詳細は後半で説明します。

Setup() -> (R::any()) SetupX(X::any()) -> (R::any()) Cleanup(R::any()) -> any() CleanupX(X::any(), R::any()) -> any() Instantiator((R::any()) -> Tests) | {with, [AbstractTestFun::((any()) -> any())]} Wherelocal | spawn | {spawn, Node::atom()}

以下の表現は、テストセットを操作するためのフィックスチャを定義します。

{setup, Setup, Tests | Instantiator}
{setup, Setup, Cleanup, Tests | Instantiator}
{setup, Where, Setup, Tests | Instantiator}
{setup, Where, Setup, Cleanup, Tests | Instantiator}
setupは、一つのフィックスチャをすべてのテストを実行するためにセットアップします。また、追加で片付けのためのコードを追加することもできます。引数についてはこの後で詳細に説明します。
{node, Node::atom(), Tests | Instantiator}
{node, Node::atom(), Args::string(), Tests | Instantiator}
nodeはsetupはセットアップと同様ですが、指定しない場合は、テストを実行したら終了するスレーブのノードを開始します。atomノードは"ノード名@完全なマシン名"というフォーマットを持ちます。Argsは新しいノード作成時に使用される追加の引数になります。詳しくはslave:start_link/3をご覧ください。
{foreach, Where, Setup, Cleanup, [Tests | Instantiator]}
{foreach, Setup, Cleanup, [Tests | Instantiator]}
{foreach, Where, Setup, [Tests | Instantiator]}
{foreach, Setup, [Tests | Instantiator]}

foreachはフィックスチャのセットアップと追加の片付けを、指定されたテストセットの中の一つ一つのテストに対して繰り返し行う際に使用されます。

{foreachx, Where, SetupX, CleanupX, Pairs::[{X::any(), ((X::any(), R::any()) -> Tests)}]}
{foreachx, SetupX, CleanupX, Pairs}
{foreachx, Where, SetupX, Pairs}
{foreachx, SetupX, Pairs}

foreachxはforeachとほぼ同じですが、テストセットではなくて、追加の引数Xと、追加のインスタンス作成関数のペアのリストに対して行われます。

Setup関数は、指定された何かテストが実行される前に実行されます。また、Cleanup関数は、指定されたテストが(理由に関わらず)もうなくなった時点で実行されます。セットアップ関数は引数を取りません。セットアップ関数の返値はそのままCleanup関数に渡されます。Cleanup関数は必要なことを行い、okアトムなどの任意の値を返す関数でなければなりません。SetupX, CleanupXもほぼ同様ですが、追加の引数X(コンテキストに依存します)を受け取ります。もしCleanup引数が設定されなかった場合には、何もしないダミーの関数が設定されます。

Instantiator関数はCleanup関数と同様に、いくつかの値(Setup関数が返した値)を受け取ります。Instantiator関数はジェネレータ(プリミティブを参照してください)と同様に。指定された値でインスタンス化されたテストを持つテストセットを返さなければなりません。特別なケースとしては、{with, [AbstractTestFun]}というシンタックスは引数を一つ取る関数のリストの一つ一つに対して値を渡すインスタンス作成関数を表します(この行自信ないです)。詳しくは、プリミティブの{with, X, [...]}の項目を参照してください。

これらの用語は指定されたテストがどのように実行されるかを定義します。デフォルトではspawnが設定されていて、現在のプロセスでセットアップと片付けが行われ、テスト自体はサブプロセスで実行されます。{spawn, Node}もspawnと近いのですが、サブプロセスは指定されたノード上で実行されます。localを指定すると、セットアップ、片付けと、テストの実行のすべてを現在のプロセスで実行します。この場合、テストがタイムアウトしてプロセスが終了したときに、クリーンアップ処理が実行されないという問題があります。そのため、ファイル操作など、永続化に関するフィックスチャについては避けるようにしてください。以下のような場合にのみ、'local'を指定するようにしてください。

  • セットアップ、片付けをテストと同じプロセスで実行する必要がある
  • プロセスが止められた場合には片付けをする必要がない場合。例えば、セットアップではプロセスの外に対しては状態の影響を与えていない場合など

1.5.7 遅延ジェネレータ

テストが開始するまでは、テスト記述のセット全体を生成するのではなく、徐々に生成できたら便利なこともあります。例えば、かなり大量のテストを生成したいが、かなりのすべてを一度に生成するにはメモリをかなり消費していまう場合などです。

呼ばれるたびに、完了していたら空のリストを作り、そうでなければテストケースと残りのテストを生成する新しいジェネレータを含むリストを返すという、ジェネレータを作成するのが簡単です。以下のコードが基本パターンでのデモコードになります。

lazy_test_() ->
    lazy_gen(10000).

lazy_gen(N) ->
    {generator,
     fun () ->
         if N > 0 ->
                [?_test(...)
                 | lazy_gen(N-1)];
            true ->
                []
         end
     end}.

EUnitが、テストを実行しようとこのテスト表現をトラバースしようとしたときに、新しいジェネレータは、前のテストが実行されるまでは次のテストを生成しません。

上記のlazy_gen/1など、補助関数を使ってこの主の再帰的なジェネレータを作成することは簡単です。名前空間を汚染するのがいやで、この種のコードを書くのが好きであれば、再帰的なfunを使っても書くことができます。

eunit 2.1.2

Copyright 2004-2009 Mikael Remond, Richard Carlsson

posted by しぶかわ at 2009/07/17 17:17 | Comment(1) | TrackBack(0) | EUnitユーザーズガイド
カテゴリ
記事一覧
ページ内インデックス
検索ボックス

最近のコメント
プロジェクト計画 by pharaohtools (06/14)
iPadがKindleに勝てない10の理由 by harga body slim herbal (02/13)
iPadがKindleに勝てない10の理由 by harga cmp pelangsing (10/28)
Pythonを使って2MBのメモリで100万の数値をソートする by 子供用ラッシュガード (08/24)
iPadがKindleに勝てない10の理由 by casino (07/30)
最近のトラックバック

Profile by iddy

Counter

Powered by さくらのブログ