メソッドチェーン形式のテスト記述ライブラリ

昨日の今日で特に更新はないのですが、せっかく画像作ったので再解説。命名は見たとおりに、メソッドチェーンで切らさずアサーションが書ける!ということから付けました。テストを、限りなくシンプルに素早く迷いなく書けるようにしたかった。その答えが、メソッドチェーンで最後に値を取ることです。

// 全てIs一つでメソッドチェーンで流るまま!
Math.Pow(5, 2).Is(25);
"foobar".Is(s => s.StartsWith("foo") && s.EndsWith("bar"));
Enumerable.Range(1, 5).Is(1, 2, 3, 4, 5);

Assert.AreEqualの最大の問題は、どっちがactualでどっちがexpectedだか悩んでしまうこと。一秒でも引っかかったら「気持よくない」のです。テストはリズム。リズムを崩す要素は極限まで潰さなければならない。Chaining Assertionならば、引数として与えるのはexpectedだけなので、全く悩む必要がなくなる。些細なことですが、しかし、大きなことです。

また、この手のメソッドチェーン式のものでよく見られるのが「流れるようなインターフェイス」を名乗って、自然言語のようにチェーンで書けるようにする、などというものがありますが、滑稽です。EqualTo().Within().And()だの.Should().Not.Be.Null()だの、馬鹿げてる。ラムダ式なら一発だし、そちらのほうが遥かに分かりやすい。DSLは分かりやすく書きやすくすることを目指すものであって、形式主義に陥ることじゃない。自然言語風流れるようなインターフェイスは二度とDSLを名乗るべきではない。

もう一つの問題は、無駄に沢山あるアサート関数。覚えるのは面倒。特に、コレクション関連。ぶっちゃけ全然扱いやすくなく、そして、私達にはずっとずっと扱いやすいLinq to Objectsがあるじゃないか。というわけで、コレクションのテストをしたい時は、Linq to Objectsで結果まで絞ってIsで書くという手法を推奨します。

new[]{1, 3, 7, 8}.Contains(8).Is(true);
new[]{1, 3, 7, 8}.Count(i => i % 2 != 0).Is(3);
new[]{1, 3, 7, 8}.Any().Is(true);
new[]{1, 3, 7, 8}.All(i => i < 5).Is(false);

ね、このほうがずっと書きやすいし柔軟に書ける。

非同期のテスト

非同期のテストは難しい。結果が返ってくるのが非同期なのでテストを抜けてしまうので。ではどうするか、答えは、Rx使えば余裕です。例として以下のコードは今こそこそと作ってるWP7アプリ用のテストです。

[TestMethod]
public void Search()
{
    // SearchLyricはWebRequestのBeginGetResponseで非同期に問い合わせるもの
    // 結果はIObservableを返す非同期なものなので、ToEnumerableして同期的に待機する
    var song = new Song { Artist = "吉幾三", Title = "俺ら東京さ行ぐだ" };
    var array = song.SearchLyric().ToEnumerable().ToArray();
    
    array.Count().Is(1);
    array.First().Title.Is("俺ら東京さ行ぐだ 吉幾三");
    array.First().Url.Is("http://music.goo.ne.jp/lyric/LYRUTND1127/index.html");
}

FirstとかToEnumerableで、非同期をサクッとブロックして同期的に待機してしまえば簡単に値を確保できてしまいます。とまあ、そんなわけで非同期処理は全部Rxで行うよう統一すると、こういうところで物凄く楽になるわけですね、素晴らしい。だからもう非同期プログラミングにRx無しとか全方位でありえないわけです。

といっても、Rxなんて使ってないし!という場合は、こんなものが。例は恣意的すぎますが

[TestMethod]
public void SpinUntilTest()
{
    int number = 0;

    // 非同期処理をしてるとする
    ThreadPool.QueueUserWorkItem(_ =>
    {
        Thread.Sleep(3000); // 重たい処理をしてるとする
        number = 1000;
    });

    // 指定条件が成立するまで待機
    SpinWait.SpinUntil(() => number != 0, 10000); // Timeout = 10秒

    number.Is(1000);
}

Pxチームの記事 SpinWait.SpinUntil for unit testing で見たのですが、SpinWait.SpinUntilが結構使えそうです。Thread.Sleepでタイムアウトいっぱいまで待つとか、手動でManualResetEventを設定する、などなどに比べると遥かにサクッと書けて良さそう。ていうかSpinWait.SpinUntilなんて初めて知りましたよ、本当にhidden gems!

まとめ

テストのないコードはレガシーコード。と、名著が言ってるので

Chaining Assertionで苦痛を和らげて、素敵なテスト生活を送りましょう。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive