D言語の単体テストが更に便利になる silly の紹介


はじめに

D言語の単体テスト機能をさらに便利にするために作られた silly というパッケージの紹介です。

これは「単体テストの実行」をリッチにするライブラリで、最近はDUBレジストリの人気パッケージでもTOP3の常連になってきています。

導入は非常に簡単ながら、ざっくり以下のことができるようになります。
ぜひ使ってみて下さい。

  • unittest ブロック毎に結果の成否をカウントする
  • unittest ブロック毎の処理時間を表示する
  • テストに名前を付ける(Naming Tests)
  • テストを複数スレッドで分散して実行する
  • テストを正規表現でフィルタする

前置き(準備)

まず適当なパスに普通にプロジェクトを作っておきます。

dub init sandbox-silly -n
cd sandbox-silly

続けて単体テストを書きます。2つ書いておきます。

app.d
import std.stdio;

void main()
{
    writeln("Edit source/app.d to start your project.");
}

unittest
{
    assert(1 + 2 == 3);
}
unittest
{
    assert(2 + 3 == 5);
}

1回実行してみます

テスト実行
dub test
結果
Running .\sandbox-silly.exe 
1 unittests passed

app というモジュールでテストが全部成功したので、 1 unittests がパスしたよ、となってます。

基本的に全部成功する前提であればこれで十分ですが、TDDやBDDだと開発中は失敗続きになるので少しわかりづらい部分もあります。

silly を導入すると、これがもっといい感じの表示になります。

導入

ではここに silly を導入していきます。
やることは2つです。

  1. dub の設定ファイルに単体テスト用の unittest という configuration を追加し、依存関係を追加する
  2. main 関数のあるプログラムであれば条件コンパイル等で除外しておく

依存関係の追加

sillyの最新バージョンが分からないので、とりあえず dub add コマンドで最新版を追加して書き換えます。

dub add silly
dub.json
{
  "name": "sandbox-silly",
  "dependencies": {
    "silly": "~>1.0.2"
  }
}

2020年6月14日現在、最新バージョンは 1.0.2 でした。

ここから dub test で使われる unittest という名前の configuration を追加し、dependencies も移動します。

また、元の実行ファイルを作る設定も残しておくため executable というのも書いておきます。こちらは default とかでも良いです。

dub.json
{
  "name": "sandbox-silly",
  "configurations": [
    {
      "name": "executable"
    },
    {
      "name": "unittest",
      "dependencies": {
        "silly": "~>1.0.2"
      }
    }
  ]
}

エントリポイントの除外

silly は独自のエントリポイントを持っているライブラリなので、アプリ側で main 関数を書かないようにする必要があります。

ライブラリであれば何もしなくて大丈夫ですが、今回は main 関数があるので version (unittest) を使い条件コンパイルで消しておきます。

version (unittest) {}
else
{
    void main()
    {
        writeln("Edit source/app.d to start your project.");
    }
}

実行

さて、導入はこれだけです。もう1回実行してみましょう。
テスト実行のコマンドは変わりません。

テスト実行
dub test

テスト結果

結果の表示が変わりました。色付きなので画像にしています。

unittest のブロック単位でカウントされ、どのテストが成功したのか失敗したのか、が色付きで表示されます。
チェックマークの直後がモジュール名、その次がテスト名(単体テストの関数名、Lの後が行数)です。

また実行時間が表示されるのもいい感じですね。

失敗時

assert(2 + 3 == 6) として失敗させてみます。

failedのほうが1つ増えました。

ちなみにVSCodeならスタックトレースのファイル名のあたりにマウスを載せるとCtrl+Clickで失敗した場所に移動できます。

と、以上が基本的な使い方です。

以下、さらに便利な部分について解説します。

機能

名前付きテスト

単体テスト1つ1つに名前を付けられます。
分かりやすい名前をつけておくと結果もわかりやすくなります。

@("1+2==3")
unittest
{
    assert(1 + 2 == 3);
}

@("...") の部分は言語的には UDA (User Defined Attribute)という機能で、関数に文字列を属性として付与しています。
silly はこの文字列の UDA をテストの名前として認識します。

文字列の UDA は言語の標準機能なので、 sillyimport したり特別覚えることもないのは良いところかなと思います。

マルチスレッド化

silly のテスト実行は、既定で複数スレッドで実行されます。

単体テストは独立順不同で実行できるという想定なので、マルチスレッド化すれば単純に早く終わるだろう、という感じです。
しかし、グローバル変数を触るようなテスト、外部ファイルを書いたりするテストはマルチスレッドだと具合が悪いので無効化したいときもあります。

無効化するときは、引数として、 -t 1--threads=1 といった感じで指定します。
0 はコア数から自動で決定、あとは好きな数を設定すればOKです。

スレッド数を指定する
dub test -- -t 1

なお、引数の指定で dub test の後に -- と挟んでいる点に注意が必要です。
dub そのものに対する引数と dub test の実行に関わる引数を分ける意味があります。

テストのフィルタリング

silly の目玉機能です。

モジュール名やテスト名に対して正規表現でフィルタリングすることができます。

含めたい場合は、 -i--include で指定、
除外する場合は、 -e--exclude で指定します。

appモジュールだけテスト
dub test -- -i app
appの1+2は除外する
dub test -- -i app -e 1\+2

dub test の後に -- と挟んでいるのに注意です

今書いてるモジュールだけテストしたいときなど、割と気軽にテストを絞れるので非常に便利です。

また名前の付け方によって優先度を表現したりできるので、時間のかかるテストを除外するなどしたいときはこのあたりで調整すると良さそうですね。

複数スレッドで実行してほしくないグループ毎に名前を付けておいて、それを -i-t 1 で調整すれば高度な制御もできるようになります。

詳細表示

-v--verbose というフラグを付けることで詳細表示モードになります。

  • テスト毎に実行時間が表示される
  • silly のテストランナーも含めたスタックトレースが表示される

遅いテストを特定できるので、除外したり高速化することで日頃の開発が更に効率的になると思います。

テストランナーの差し替え

D言語は、単体テストのテストランナー切り替え機能をランタイム機能として提供しています。

Runtime.extendedModuleUnitTester = &customTester; という感じで差し替えるだけでよく、自力で書くのも簡単です。

以下にサンプル含めたドキュメントがあるので、興味がある方は参照してみてください。