D言語 Advent Calendar 2013の三日目の記事です。
更に、Aizu Advent Calendar 2013の三日目の記事でもあります。
D言語には、簡単な単体テスト機能が言語仕様に含まれています。そのおかげで、気軽に関数やクラスの単体テストを書くことが出来ます。
その一方、D言語にはコンパイル時関数実行(CTFE)という機能があります。名前の通り、コンパイル時に関数を実行する機能です。今となっては、「標準ライブラリのこの関数がCTFE出来ない!これはバグだ!!」とも言われるほど、関数がCTFE出来ることは重要になりつつあります。
関数がCTFEableかどうか、またはCTFEにおいて正常に動作するか、単体テストを使って、実行時と同じようにチェック出来るととても助かります。が、残念ながらD言語の単体テストは実行時にしか実行してくれません。
__traits(getUnittests)とは
D言語には、traitという、コンパイル時にコンパイラの内部情報を取得する機能があります。その中に、getUnitTestsという命令があり、それを使うと、特定のモジュールに書かれている単体テストを取得することが出来ます。
module test;
import std.stdio;
unittest
{
"Test A".writeln();
}
unittest
{
"Test B".writeln();
}
void main()
{
"Start main".writeln();
foreach(test; __traits(getUnitTests, mixin(__MODULE__)))
{
test();
}
}
__MODULE__
は、その場所のモジュール名が書かれた文字列リテラルとなります。D言語には、mixinと呼ばれる、文字列をD言語のソースコードとして解釈する機能があります。よって、__MODULE__
をmixinすることによって、mixin(__MODULE__)
はその場所のモジュールを指すことになります。
$ rdmd -unittest test.d
Test A
Test B
Start main
Test A
Test B
通常通り、main関数が呼び出される前に単体テストが呼ばれています。それに加えて、getUnitTestsで取得した関数を呼ぶことによって、もう一度単体テストが呼ばれているのが分かります。このgetUnitTestsを使うと、簡単にコンパイル時単体テストを実現することが出来ます。
__traits(getUnittests)を使ったコンパイル時単体テスト
D言語には、テンプレートmixinと呼ばれる、テンプレートの中身をmixinされた場所に展開する機能があります。そのようなテンプレートをmixinテンプレートと呼びます。そのmixinテンプレートを使い、テンプレートmixinすると、そのモジュールの単体テストが全てコンパイル時にも実行される、というようなmixinテンプレートを作ってみます。
module compile_time_unittest;
mixin template enableCompileTimeUnittest(string module_ = __MODULE__)
{
static assert(
{
foreach(test; __traits(getUnitTests, mixin(module_)))
{
test();
}
return true;
}());
}
テンプレート仮引数のデフォルト値として使われた__MODULE__
は、インスタンス化した場所のモジュール名の文字列リテラルとなります。static assertは、中身をCTFEし、その結果がtrueかどうかをコンパイル時にチェックする機能です。static assertの中身は、{ ... }()
というように、関数リテラルを作り、即座に呼び出しています。関数リテラルの中は、上のコードと同じように、このテンプレートがmixinされたモジュールの単体テストを取得し、それを読んでいます。
さっそく、このenableCompileTimeUnittestを使ってみましょう!
module test2;
import compile_time_unittest;
mixin enableCompileTimeUnittest;
unittest
{
assert(false);
}
void main(){}
$ rdmd --build-only -unittest test2.d
test2.d(9): Error: assert(false) failed
...
rdmdは、ソースコードからimportなどの情報から依存情報を抜き出し、自動でファイルを追加してくれるので、test2.dだけの指定で十分です。rdmdに--build-onlyを渡すことによって、コンパイル後に実行しないようにしました。見事にコンパイルが失敗しています!! コンパイル時に単体テストが動きました!!!
まとめ
正直に言って、上の方法でなくてもコンパイル時に単体テストを実行する方法はあります。ですが、他の方法と比べて、上の方法は、実行時単体テストを書くのと全く同様にコンパイル時単体テストを書ける点が優れてると思われます。宣言に情報を追加することの出来る、UDAと呼ばれる機能を使うと、コンパイル時のみの単体テストや、実行時のみの単体テストを作ることも可能です。
D言語を使っていると、必ずコンパイル時単体テストが必要になる時が来ます!そんな時、ぜひ上のような方法を試してみてください。