C# におけるユニットテストでの Fluent Assertions
[[academy-video-youtube({"vid": "TytferBCLOo", "start_time": "0", "title": "C# におけるユニットテストの流暢なアサーション", "creator": "Tim Corey", "length": "10m 6s"})]]
ユニットテストは一度書かれ、その後何度も読まれるため、アサーションラインはテストスイートにおいて最も重要な詳細の一つです。Assert.Equal(expected, actual)は機能しますが、単に2つの文字列を比較する失敗メッセージは、テストが何を確認していたのかを常に説明するものではありません。 流暢なアサーションはアサーションの構文を改変し、失敗メッセージを文章にするので、アサーション行がその規則を強制するように読み取られます。
彼のビデオ"ユニットテストにおけるC#のFluent Assertions"で、Tim CoreyはすでにSampleClassハッピーパスを行使するxUnitプロジェクトを取り上げ、エディ・ヴァン・ヘイレンという境界例を導入して、単純なスペースでの分割ロジックを破綻させます。 彼はFluent Assertionsパッケージをインストールし、AssertionScopeで終了し、単一のテストが最初の失敗に止まらずすべての失敗条件を一度に報告するようにします。 Bare equality diffs を読むのが難しくなるほどのテストスイートを持つチームは、ここにアップグレードパスを見つけます。
xUnit スタート地点
[0:35 - 2:46] 画面上のプロジェクトは小さく、フルネームをコンストラクタで受け取り、スペースでSampleClassを含むクラスライブラリと、ハッピーパスを行使するxUnitテストプロジェクトから成ります。 テストはTheoryを使用してInlineData属性と共に、2つの入力("Tim Corey"と"Sue Storm")に対して同じロジックを実行し、各アサーションは標準のAssert.Equal(expected, actual)形式です。
[Theory]
[InlineData("Tim Corey", "Tim")]
[InlineData("Sue Storm", "Sue")]
public void TestFirstNameProperty(string fullName, string expected)
{
var sample = new SampleClass(fullName);
Assert.Equal(expected, sample.FirstName);
}[Theory]
[InlineData("Tim Corey", "Tim")]
[InlineData("Sue Storm", "Sue")]
public void TestFirstNameProperty(string fullName, string expected)
{
var sample = new SampleClass(fullName);
Assert.Equal(expected, sample.FirstName);
}スイートを実行すると、6 つのテストが成功します。 これは何も間違っていませんが、"最初のスペース、最後"という形に適合するケースに対しては、アサーションは十分に明確に読めます。 興味深い領域は名前がその形に適合しない場合から始まります。
ハッピーパスが機能しなくなる場合
([2:46 - 4:32])Tim が選んだエッジケースは"Eddie Van Halen"です。これは姓が"Van Halen"である 3 語の名前であり、単にスペースの後のトークンではありません。 現在の実装は分割されたインデックス1を取得してそれを姓としていますので、クラスは"Van"を姓として返し、"Halen"を完全に破棄します。 バグは実際に存在し、失敗テストを書くことはそれを修正する第一歩です。
Assert.Equal("Van Halen", sample.LastName)でそのテストを書くことは機能しますが、失敗メッセージは文脈のない文字列比較のように読まれます。 流暢なアサーションに切り替えると、同じテストはルールを言葉で表現し、テスト対象のプロパティを名前で示す失敗メッセージを生成します。小さなスイートでは違いは化粧的に見えますが: 数百のアサーションがあるスイートでは可読性の差が大きくなります。
Fluent Assertions のインストール
[4:32 - 5:10] パッケージはNuGetでFluentAssertionsの下にあります。 Tim はそれがレジストリで最もダウンロードされたパッケージの一つであることを指摘し、どの新規の寄稿者も統計的にそれを見たことがある可能性が高いとしました。 テストプロジェクトは上部に 2 つの using ディレクティブを追加します:
using FluentAssertions;
using FluentAssertions.Execution;using FluentAssertions;
using FluentAssertions.Execution;最初のものはアサーション拡張メソッドを導入します。これはほとんどのテストが必要とするものです。 2番目のものはビデオの後半で取り上げられるAssertionScopeタイプのために存在します; アサーションをグループ化しないテストはそれを省くことができます。
Should.Be の構文
([5:10 - 6:14])Van Halen テストの最小 Fluent Assertions 書き直し:
[Fact]
public void TestEdgeCaseNames()
{
var sample = new SampleClass("Eddie Van Halen");
sample.LastName.Should().Be("Van Halen");
}[Fact]
public void TestEdgeCaseNames()
{
var sample = new SampleClass("Eddie Van Halen");
sample.LastName.Should().Be("Van Halen");
}チェーンは話し言葉の英語に近く読み取れます。 .Should()拡張は比較を説明するメソッドを持つアサーションオブジェクトを返します; .Be(...)は等価性チェックを行います。 ケースセンシティビティ、前方および後方の空白はすべてそのチェックに含まれ、生産コードのほとんどの文字列比較の動作と一致します。
壊れた実装に対してこのテストを実行すると、プロパティ名とギャップを説明する失敗メッセージを生成します:"サンプル.LastNameが9の長さで"Van Halen"であることを期待しましたが、"Van"は3の長さでした。"このメッセージはその値を生成した式でテスト中の値を特定し、Assert.Equal形式が提供しない文脈の種類を示します。
複数条件のチェーン
([6:14 - 8:10])単一のプロパティはしばしば複数のルールを満たす必要があります。 Fluent Assertionsは.Andで条件を構成し、チェーンが1つのステートメントに留まるようにします:
sample.LastName.Should()
.StartWith("Van")
.And.EndWith("len")
.And.Contain(" ");sample.LastName.Should()
.StartWith("Van")
.And.EndWith("len")
.And.Contain(" ");チェーンの各リンクは個別の条件です。 デフォルトでは、チェーンは最初の失敗で報告を停止します:EndWithを名前付けし、そこでアサーションは停止します。 テストで各条件が独立し、すべての失敗を浮き彫りにしたい場合は、次のセクションのアサーションスコープがその動作を変更します。
連鎖構文は意図を表現することを重視し、等価性を列挙することを重視しません。 "姓は Van で始まり、len で終わり、スペースを含む必要がある"というテストは、仕様書のように見えます。 3つのAssert.True呼び出しで書かれた同じロジックは、物語をつなぐもののない3つのブールチェックとして読まれます。
アサーションスコープを使用してすべての失敗をレポートする
([8:10 - 9:34])最初の失敗でレポートを止めるデフォルトの動作は、後の条件が前提条件に依存するチェーンには適しています。 すべての条件が独立して重要であるチェーンには、アサーションをAssertionScopeに包むことで、1回の実行でテストがすべての失敗を報告します:
[Fact]
public void TestEdgeCaseNames()
{
var sample = new SampleClass("Eddie Van Halen");
using var _ = new AssertionScope();
sample.LastName.Should().StartWith("Van");
sample.LastName.Should().EndWith("len");
sample.LastName.Should().Contain(" ");
}[Fact]
public void TestEdgeCaseNames()
{
var sample = new SampleClass("Eddie Van Halen");
using var _ = new AssertionScope();
sample.LastName.Should().StartWith("Van");
sample.LastName.Should().EndWith("len");
sample.LastName.Should().Contain(" ");
}using-var-discard ラインは標準パターンです:スコープはメソッドの終わりで変数がスコープから外れるまで持続し、ディスカードアンダースコアは変数自体が読み取られないことを示します。 テストが失敗すると、メッセージには各失敗条件が別々の行として含まれ、1 回の実行で全体の話が伝わることになります。3 問題を浮かび上がらせるために 3 回回す必要がある場合は、ストーリーが 3 回に渡ります。 出力されたオブジェクトの形状を多くのプロパティにわたって検証するテストでは、1 つのバグを各テストサイクルで修正するだけでなく、すべてを一度に修正できます。
まとめ: 仕様書のように読めるテスト
([9:34 - 10:06]), Fluent Assertions はテストが確認する内容を変更することはありません。 検証の読み方を変えます。 AssertionScopeグループ化はすべて、テストコードをテスト中の振る舞いの仕様書に近づけます。入力検証に使用されるFluentValidationと同様のフルエントスタイルと組み合わせることで、新参者がツアーなしで読めるテストスイートと検証規則を生み出します。
結論
[9:34 - 10:06] テストプロジェクトにFluent Assertionsを追加するには、1つのNuGetインストール、2つのusingディレクティブ、およびactual.Should().Be(expected)へのアサーションラインの書き直しが必要です。 チェーンは複数の条件を単一のステートメントで表現し、AssertionScopeは単一のテストが最初の失敗で停止せず、すべての失敗を報告します。利益は、破られた規則を説明する失敗メッセージであり、これはスタックトレースと文章の違いです。
例のヒント:コレクションのアサートを行う場合、プロパティごとの比較よりもresult.Should().BeEquivalentTo(expected)を優先してください。 オブジェクトグラフを歩き、パスによる最初の差異のあるプロパティを報告します(例えば、users[2].Address.City)、これは50要素のリストが1つのネストしたフィールドで意見が合わない場合、"コレクションは等しくない"メッセージよりもはるかに有用です。
彼の YouTube チャンネルで、ビデオの全編を視聴し、10 分トレーニングシリーズで読みやすく、維持可能な C# のテストを書くためのさらなる洞察を得てください。

