C#(.NET)のテストフレームワーク4種の比較雑感

for MSTestをやめて、NUnitとMBUnitとxUnit.NETにも対応しました。MSTestに限定していたのは、単純に他のを入れて試すの面倒くせー、というだけの話であり、そういう態度はいけないよね、と思ったので全部入れました。NUnitはDocumentだけは読んでかなり参考にしてたのですが、他のは全くはぢめて。MSTest以外はみんな野心的に開発進んでるんですね。比べると機能面では一番見劣りするMSTest。

というわけで、対応させるために各種フレームワークを入れる&多少触ったので、それらの紹介/感想などを書きたいと思います。C#上というか.NET上、ですね。の前に、更新事項が幾つかあるのでそれを。まず、CollectionAssertに等値比較するラムダ式を受けるオーバーロードを追加しました。

var lower = new[] { "a", "b", "c" };
var upper = new[] { "A", "B", "C" };

// IEqualityComparer<T>かFunc<T,T,bool>を渡して、コレクションの任意の演算子で等値比較
lower.Is(upper, StringComparer.InvariantCultureIgnoreCase);
lower.Is(upper, (x, y) => x.ToUpper() == y.ToUpper());

// Linq to ObjectsのSequenceEqualを使えば今までも出来なくもなかったのですが!
lower.SequenceEqual(upper, StringComparer.InvariantCultureIgnoreCase).Is(true);

SequenceEqual使えばいいぢゃん(キリッ って思ってました。Isのオーバーロードがかなり嵩んでいて、オーバーロードのIntelliSense汚染が始まってる。IntelliSense = ヘルプ。であるべきなのですが、数が多かったり似たようなオーバーロードが多いと、混乱を招いてしまい良くないわけです。だいたいがして、以前にAllを拒んでいたのにSequenceEqualはアリなのかよ、という。一応弁解すると、CollectionAssert自体に比較子を受けるオーバーロードがあるので、IsがCollectionAssertを示すのなら、それを使える口を用意すること自体は悪くないかな、と。あと、SequenceEqualはIEqualityComparerなのが使いづらい。ラムダ式を渡せる口がないので、こちらに用意して回避したかったというのもあります。AnonymousComparer - lambda compare selector for Linqを使えばラムダ式渡せますが。

それにしても、本来のものはIComparerを受けるんですよ、ジェネリックじゃないほうの。今時ノンジェネリックを強要とか、しかもIComparer<T>とIComparerは別物なのでIComparer<T>として定義すると使えないという。ありえない!そもそも何故にIComparerなのか、という話ではある。大小比較じゃないので、等値として0しか判定に使ってませんもの。それならIEqualityComparer<T>だろ、と。GetHashCodeの定義が(不要なのに)面倒だから、そのせいかなー。 そう考えると分からなくもないので、Func<T, T, bool>を用意しておきました。ラムダ式は最高よね。

それと、今回、コレクションでのIsの動作を変更しました。今までは欠陥があって、例えばlower.Is(upper)とすると、オーバーロードがかち合ってCollectionAssertではなくAreEqualsのほうで動いてしまってました。これは、大変望ましくない。オーバーロードだけだと解決しようがないので、内部で動的にIEnumerbaleかを判定した上でCollectionAssertを使うようにしてやりました。

例外のテスト

他のテストフレームワークを色々見たのですが、例外は属性じゃなくてラムダ式でのテストをサポートするのも少なくない。というかMSTestだけですが、それがないのは。属性だと、範囲が広すぎる(メソッドをテストしたいのに、コンストラクタがテスト範囲に含まれ、コンストラクタが例外を吐いてしまったらメソッドのテストにならない)問題があるので、ラムダ式で書けるほうがいい、という流れのようで。その他に1例外テスト1メソッドが強制されて面倒くさいー、引数のチェックぐらい、1メソッドに全部突っ込んでおきたい、というモノグサな私的にもラムダ式のほうが好み。というわけで、移植移植。

// Throes<T>で発生すべき例外を指定する
AssertEx.Throws<ArgumentNullException>(() => "foo".StartsWith(null));

// 戻り値も取得可能で、その場合は発生した例外が入ってます
var ex = AssertEx.Throws<InvalidOperationException>(() =>
{
    throw new InvalidOperationException("foobar operation");
});
ex.Message.Is(s => s.Contains("foobar")); // それにより追加のアサーションが可能

// 例外が起きないことを表明するテスト
AssertEx.DoesNotThrow(() =>
{
    // code
});

これを入れるのに伴い、拡張メソッド置き場のクラス名をAssertExに変更しました。それに加え、partial classにしたので、独自に何かメソッド置きたい場合に、AssertExに追加することが可能です。.csファイル一つをポン置きでライブラリと言い張るからこそ出来るやり口……。割とどうでもいい。

テストフレームワーク雑感

Assertちょっと触った程度でしかないので、本当に雑感ですが、一応は各フレームワークを触ったので感想など。

非常に何とかUnitっぽい仕上がり。おお、これこれ、みたいな。ある種のスタンダード。他のテストフレームワークにあるあの機能が欲しいなあ、みたいなものもしっかり取り入れてる感で、全く不足なく。情報も豊富、周辺環境も充実。

不満は、Assert.That。これJavaのJUnit発祥なのかしらね。Hamcrest?まあしかしともかく、酷い。英文のようで読みやすいとか入力補完ですらすらとか、ないない。これが本当に読みやすい?書きやすい?ありえないでしょ……。Is.All.GreaterThanとか、ただのシンタックス遊び。ラムダ式のないJava(or .NET2.0)ならともかく、現在のC#でそれやる意味はどこにもない。

かなり独自な方向に走っている印象。Gallioというプラットフォーム中立なテストシステムを立てて、その上にNUnitやMSTest、そしてGallioの前身でありテストフレームワークのMbUnitなどが乗っかる。という、壮大なお話。IronRubyでRSpec、など独自にテストシステムを立てられるほどに需要がなさそうな、でもあると絶対嬉しいと思う人いるよね、といったものを一手に吸収出来るかもです(実際RSpecは乗っかってる模様)。そんな壮大な話を出すだけあって、テストランナーとしてのGallioの出来はかなり良いように見えます。

MbUnit自体は可もなく不可もなく。属性周りとかは独特なのかなあ、単純なアサート関数しか使っていないので、その辺は分かりません。

ちなみに、今回紹介するテストフレームワークの中で、唯一NuGet非対応。対応に関しては議論されたようですが、どうもGallioのプラグインを中心とする依存関係が、現在のNuGetだと上手く対応させられないそうで。将来のNuGetでも対応するような仕組みへの変更は今のところ考えてない、とNuGet側から返答を貰っているみたいなので、当面はMbUnitのNuGet入りはなさそうです、残念。まあ、若干大掛かりなGallioのインストール込みで考えたほうが嬉しいことも多く、NuGet経由での必要性は薄いから、それはそれでしょうがないかな、といったところ。

非常に独特で、旧来のテストの記述法を徹底的に見直して新しく作り直されています。とっつきは悪いのですが(如何せん、他と比べて全然互換がない)良く考えられていて、素晴らしいと思います。

例えば、CollectionAssertはなく、Assert.Equalが万能に、Objectの場合はEqualsで、Collectionの場合は列挙しての値比較で行ってくれます。つまり、この辺はChaining AssertionのIsと同じ。旧来のしがらみに囚われず、Assert関数はどういう形であることがベストなのか、ということを突き詰めて考えると、そうなる。と思う。

ただ、非常に厳密な比較を行うので、型が違う(IEnumerable<T>とT[]とか)とFailになります。Chaining AssertionのIsは、ゆるふわに列挙後の値比較だけで判定します。どちらが良いのかは、正しいテスト、ということを考えればxUnit.NETのほうなのでしょう。私は、その辺は、とにかく「書き味」優先で振りました。型比較の厳密さは例外テストのThrowsメソッドにも現れていて、MSTestやMbUnitは派生型も含め一致すればSuccessとしますが、xUnit.NETは厳密に一致しないとFailになります。Chaining AssertionのThrowsは厳密一致のほうを採用しました。

正しいテストを書くために、テストフレームワークはかくあるべき、という強い意志でもって開発されている感じ。これは、相当に良いと思います。MSTest以外のものを使うなら、私はこれを選びたい。付属のテストランナーは貧弱ですが、Gallioを使うことで克服出来ます。

  • MSTest

唯一TestCaseがない。これがたまらなく不便なんです!かわりに強力なデータソース読み込みが可能になっているようですけれど、強力な分、セットアップに手間がかかってダルいという。他は、いたって普通。ふつーのAssert関数群とふつーの属性でのテスト設定。このノーマルっぷりは標準搭載らしいかもです。最大の欠点はウィザードで自動生成されるテンプレコードがどうしょうもなくクソなこと。あのノイズだらけのゴミは何とかすべし。

最大のメリットは完全なVisual Studio統合。サクッとデバッグ実行。何て素晴らしい。標準搭載で準備一切不要なのも嬉しい。昨今のテストの重要度を考えると、Express EditionにもMSTest入ってるべきですねえ。ちょっと弱いAssert関数群とTestCaseがないことは、Chaining Assertionを使えば補完されるので、全体的に割と問題なくなって素敵テストフレームワークに早変わり。TestCaseに関してはモドきなので若干弱いけれど。

番外編。僕と契約してPexをより強固にしようよ!Code Contracts + Pexは後ろにMicrosoft Researchがいる.NETならではの強烈さだなあ、と。とりあえずVS2010 後の世代のプログラミング (2) Pex « kazuk は null に触れてしまったの素晴らしい導入記事を読んで試せばいいと思うんだ。そういえば、Pex開発者はMbUnitのプロジェクト創設者(学生時代にMbUnit作って、その後MSRに行ったそうで、凄いね...)だそうですよ。

比較しての相違点

他のテストフレームワークや拡張補助ライブラリと根源的に異なるのは、CollectionAssertに対する考え方です。Linq to Objectsに投げればそれが一番でしょ?という。末尾に.Isなのは、それが一番Linq to Objectsを適用して返した場合に書きやすいから。Linqはインフラ。これを活用しないなんて勿体無い。ドキュメントにそれを書くことで、公的に推奨している、ということを押し出している、つもりです。

まとめ

xUnit.NETはかなり素晴らしいんじゃないかと思います。IDE完全統合という点で、私はMSTestを選んでしまいますが、MSTest以外から使うものを選択するのならば、xUnit.NETにしたいところ。周辺環境がまるで整ってない感はありますが、その辺はGallioを使えば吸収出来るっぽいので、セットで、みたいなところかしらん。

テストは別に同一言語である必要はない、ので、CLRの特色を活かせば、IronRubyでRSpecというのは魅力的な道に見えます。今回は試せていないのですが、いつか試してみたい。F#を用いてのテストフレームワークも良さそう。F#は、特にテストのような小さい単位ではF# Interactiveに送りながら書けるのが強烈なアドバンテージになるよね。ユニットテストぐらいの小さな単位なら、デバッガよりもむしろこちらのほうが小回り効いて書きやすそう。

私的にはChaining Assertionはそれらと比べても、全く引けを取らない書きやすさがあると思っています。C#はクラス定義などを除けば、コード本体自体の書き味はライトウェイトなのですよ、ね、ね!

それにしてもAssertThat一族の毒はどうにかならないのかねえ……。AssertThatがない、という点だけでxUnit.NET一択ですよほんと。F#などでも、この形式を踏襲しているのを見ると大変モニョる。こういうのがビヘイビアドリブンなんですかね?ただのシンタックス遊びなだけで、そーいうの違くない?って。でもAssertで詳細なデータが出ない?Expression Treeを解析して詳細なデータを出しゃあいいわけで。どっかの言語の仕組みを持ってこないで、C#の持つ特色、強みであるExpression Treeを活かす方向で動けないものなのか。

別にJavaだからどうとか言うつもりではないです。大切なのはあらゆる言語を見つめて、良いものを取り入れることでしょう?例えば、Groovyの素晴らしいPower AssertをC#でやろうとするプロジェクト expressiontocode など。C#に取り込もうとするなら、こちらのやり方でしょう。時系列を考えればそれは違う、という話もあるでしょうけど、現在の、これからの姿勢として。本当に良いものは何なのかを、考えよう。それを実現するための力がC#には備わっているのだから。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

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

Twitter:@neuecc GitHub:neuecc

Archive