Chaining Assertion ver 1.1.0.1

リリースから、意外にも色々と反響を頂き、ありがとうございました。そんなわけで、意見を考慮した結果、メソッド増えました。当初のスタンスは、シンプルにIsのみでやる、というところだったので、現在結果的に4x2つある、という状況は理想的には望ましくないのですが、現実的にはしょうがないかな、といった感です。以下が、Chaining Assertionの全拡張メソッド。

Is
IsNot
IsNull
IsNotNull
IsInstanceOf
IsNotInstanceOf
IsSameReferenceAs
IsNotSameReferenceAs

IsNotはまんまで、AreNotEqualです。IsSameReferenceAsはAreSameで、参照比較。メソッド名長いね!ただ、私はAreSameという名前は曖昧で何がSameなのか(値がSameとも取れるじゃないか!)よく分からず嫌いだったので、Referenceをメソッド名に入れるのは確定でした。その上で、IsReferenceEqualsにするかで悩みましたが、ここはAreSameであることを想起させるためにも、Sameを名前に含めることにしました。

拡張のジレンマ

最も多く使う部分のみに絞って最少のものを提供する。という思想がありました。だからNotもSameも頻度的には下がるから不要で、それらが使いたい場合はAssert.AreSameといったものを使えばいい、と。従来のものを完全に置き換えるものじゃなく、要点を絞って、処方を出す。

でもまあ、Isのみにこだわりすぎるのって、逆に良くないよね、と。事実妥協してIsNull/IsNotNullは入れていたわけだし、もう少し妥協したっていいのではないかと。と、たがが外れた結果、増量しすぎた……。これだけ増えると、Isだけで済むんだよ!簡単だよ!強力だよ!という文句に説得力に欠けていってしまう。

なので、このまま拡張し続ける気はありません。メソッドが増えると、学習コストの増加とIntelliSense汚染が出てきてしまうので、便利メソッドを増やすのは、ないかな、と。特にStringやCollection周りには、Isのような汎用的なものではなく、もっと特化的なものがあれば「便利」なのは違いないのですが、でも、それ本当に便利かな、って。ラムダ式を使えば十分代替出来る、Linq to Objectsを使えば十分代替出来る。なら、それでいいよね、と。

例えば seq.All(pred).Is(true) が失敗する時、これだとどの値がfalseになったのか分からない。判定をLinq to ObjectsのAllに任せているから。これがもし、 seq.IsAll(pred) といったような、専用メソッドが用意されていれば、細かいエラーメッセージを出すことが出来て便利なのは間違いない。

でもAllだけ?他のは?Allだけ特別扱いする理由もない。じゃあ全部のLinq to Objectsを実装するか、といったらありえない!実装の手間・俺々実装によるバグ混入の可能性、そしてIntelliSense爆発。学習コストの増大(まあ、IsXxxでXxxをLinq to Objectsのものと同一にする、という原則を貫けば増大はしませんが)。

// ソート済みであるかどうかのチェック(NUnitのIsOrderedにあたるもの)を、Linq to Objectsで
var array = new[] { 1, 5, 10, 100 };
array.Is(array.OrderBy(x => x));

Linq to Objectsの汎用性の高さ!既存の仕組みに乗っかれる場合は、全力で乗っかりたい。DynamicJsonもそうで、扱いづらいJsonReaderWriterFactoryにdynamicの皮を被せているだけにすぎない、という。

まとめ

Assert.Hogeよりも圧倒的に書きやすく美しく短くなるというほかに、圧倒的に読みやすくなります。読みやすさはメンテナンス性の向上に繋がる。一生懸命テストを書いたはいいものの、面倒くさくてメンテ放置になって、ゴミの山になってしまった。という経験が、私はある(えー)。そんなわけで、書きやすさは何より大事だし、読みやすさまで手に入るなら僥倖だし、かつ、学習コストも最小限。という売り文句なので、是非試してみてもらえればと思います。

// 今回追加された細々としたもの

// Not Assertion
"foobar".IsNot("fooooooo"); // Assert.AreNotEqual
new[] { "a", "z", "x" }.IsNot("a", "x", "z"); /// CollectionAssert.AreNotEqual

// ReferenceEqual Assertion
var tuple = Tuple.Create("foo");
tuple.IsSameReferenceAs(tuple); // Assert.AreSame
tuple.IsNotSameReferenceAs(Tuple.Create("foo")); // Assert.AreNotSame

// Type Assertion
"foobar".IsInstanceOf<string>(); // Assert.IsInstanceOfType
(999).IsNotInstanceOf<double>(); // Assert.IsNotInstanceOfType

ようするところ、AssertThatを更に凝縮したような感じです。また、 Assert.That(array, Is.All.GreaterThan(0)) なんてのは array.All(x => x >= 0).Is(true) のほうがずっと良くない?って。思うわけです(但し、Assert.Thatはエラーメッセージがずっと親切)。なお、Is.All.GreaterThanは、一見IntelliSenseが効いて書きやすそうに見えるけれど、無駄な連鎖によりイライラさせられるだけで別に全然書きやすくない。連鎖において、一つ一つの処理に重みがないものは、どうかなあ。ただのシンタックス遊びだよ、こんなの(ラムダ式のないJavaでの苦肉の策だというのなら分かる)。

それと、基本的にオブジェクトへの拡張メソッドというのは、影響範囲が広すぎて禁忌に近いわけですが、ユニットテストという範囲が限定されている状況なので、許容できるのではないかな、と思っています。しかし8つはなぁ、多すぎかなあ……。今も、これで良かったのか相当悩んでます。InstanceOfは不要だったかも、typeof書きたくなかったというだけじゃ理由としてアレだし、メソッド増やすほどの価値はあったのか、多分、ないよね、うぐぐ。でも、これでほぼ全てがチェーンで書けるようになりました。それはそれでヨシと思うことにします。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

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

Twitter:@neuecc GitHub:neuecc

Archive