3-c2. Test Test Test

初出: 2007/04/28
最新: 2007/09/14
[3. D言語各論] に戻る

目次

D言語の言語/コンパイラ組み込みの単体テスト支援機能についてまとめてみました。

単体テストを書く

HelloWorld というクラスを作ることになりました。

module helloworld;

class HelloWorld
{
}

"Hello" という文字列を返す hello メソッドと、 "World" という文字列を返す world メソッドを実装しなきゃいけないそうです。 つまり、以下のようなテストコードが通るクラスを作るのが目標です。

module helloworld;

class HelloWorld
{
    unittest
    {
        auto obj = new HelloWorld;
        assert( obj.hello() == "Hello" );
        assert( obj.world() == "World" );
    }
}

Dでは、unittest というブロックの中にテストコードを書きます。あるいは…

module helloworld;

class HelloWorld
{
    unittest
    {
        auto obj = new HelloWorld;
        assert( obj.hello() == "Hello" );
    }

    unittest
    {
        auto obj = new HelloWorld;
        assert( obj.world() == "World" );
    }
}

適当に分けて書いてもいいです。もしくは

module helloworld;

class HelloWorld
{
}

unittest
{
    auto obj = new HelloWorld;
    assert( obj.hello() == "Hello" );
    assert( obj.world() == "World" );
}

クラスの外でもunittestは書けます。普通の関数/テンプレートなどのテストや、 モジュール全体のテストみたいなものは外に書く方が自然かもしれません。 DMD付属の標準ライブラリのソースを見てみると、 unittestブロックが実際に使われている例がたくさん見つかると思います。

テストする

とりあえずテスト通らない例として無茶苦茶な実装しておきます。

module helloworld;

class HelloWorld
{
    string hello() { return "World"; }
    string world() { return "Hello"; }

    unittest
    {
        auto obj = new HelloWorld;
        assert( obj.hello() == "Hello" );
        assert( obj.world() == "World" );
    }
}

これをテストするには、てきとうに空のmain関数を書いたソースを1個用意して

// testrunner.d
void main() {}

-unittest オプション付きで、さっきのモジュールと一緒にビルドします。

dmd -unittest testrunner.d helloworld.d

そして実行

> testrunner
Error: AssertError Failure helloworld(11)

怒られました。helloworldモジュールの11行目でテスト失敗してるそうです。

dmd の -unittest オプションは、「mainを実行する前に、プログラム内の全unittestブロックを実行するように コンパイルする」という意味のオプションです。ここではunittestだけを実行するために空のmain関数を別途用意 しました。普通にプロジェクトをビルドするときに-unittestをつけるようにして、 「全部テスト通らないと起動しない」プログラムを作っちゃうこともできます。

テストのテスト

    string hello() { return "Horld"; }
    string world() { return "Wello"; }

ちゃんと直せば

> testrunner
>

何もエラーが出なくなります。OK。ちょっと寂しいのでtestrunnerのmainでなんかメッセージ出した方が楽しいかもですね。

さてさて、仕様変更です。HelloWorld クラスには、"GoodBye" という文字列を返す goodbye メソッドも 必要ということになりました。さてさてメソッドを追加しましょう。

module helloworld;

class HelloWorld
{
    string hello() { return "Hello"; }
    string world() { return "World"; }
    string goodbye() { return "GoodBey"; }

    unittest
    {
        auto obj = new HelloWorld;
        assert( obj.hello() == "Hello" );
        assert( obj.world() == "World" );
    }
}

テストテスト。

> testrunner
>

無事テストも通って…あれ?なにかおかしいですね。そう、goodbyeメソッドがテストされてません。 「単体テストされてない部分が残ってる」、これはいけません。

こういうテスト漏れを検出するには、「カバレッジ解析」が有効です。-cov オプションをつけます。

dmd -cov -unittest tester.d helloworld.d
> testrunner
>

こう実行すると、

> ls *.lst
helloworld.lst        testrunner.lst

二つ新しいファイルができてます。中身はこんなの。

       |module helloworld;
       |
       |class HelloWorld
       |{
      1|    string hello() { return "Hello"; }
      1|    string world() { return "World"; }
0000000|    string goodbye() { return "GoodBey"; }
       |
       |    unittest
       |    {
      1|        auto obj = new HelloWorld;
      1|        assert( obj.hello() == "Hello" );
      1|        assert( obj.world() == "World" );
       |    }
       |}
helloworld.d is 83% covered

左側に、「ソースの各行が何回実行されたか」の回数が表示されます。goodbyeメソッドの実行回数が0回、 つまり1度も実行されていない、つまり、1度もテストされていない ことがわかります。 カバレッジ解析によって、こういう風に、ちゃんと単体テストでクラス全体をカバーできているかを検査できます。

    unittest
    {
        auto obj = new HelloWorld;
        assert( obj.hello() == "Hello" );
        assert( obj.world() == "World" );
        assert( obj.goodbye() == "GoodBye" );
    }
}

さて、あわててテストを追加…

> testrunner
Error: AssertError Failure helloworld(14)

怒られました。無事バグが検出できたようです。GoodBeyじゃなくてGoodByeって返さないといけなかったのです。

テストされるもの

D言語でのテストは、全て assert 文で書くのが基本です。

assert( bool式 );

と書くと、bool式がfalseになっちゃてたときにエラーになります。

assert( bool式, "めっせーじ" );

と書くと、エラーの時に"めっせーじ"が表示されて多少わかりやすくなります。

assert( オブジェクト );
assert( オブジェクト, "めっせーじ" );

と書くと、オブジェクトがnullでないことと、 オブジェクトがちゃんと クラス不変条件 を満たしていることが チェックされます。おかしくなってたらエラーです。

[3. D言語各論] に戻る

presented by k.inaba   under NYSDL.