🐥

【C# .NET】Fluent AssertionsのAssertionメソッドについて整理する

2024/12/21に公開

概要

https://fluentassertions.com/

Fluent Assertionsは、C# .NETでテストコードを書く際に利用されるライブラリです。テストの検証部分を英語の文章のように直感的かつ読みやすく記述できます。たとえば、「この値が期待通りである」や「この例外がスローされた」といったことを、簡潔で自然な形で表現可能です。

「なぜFluent Assertionsを使うのか?」については、公式ドキュメントのWhyに分かりやすく書かれていますので、そちらをご覧いただければと思います。

https://fluentassertions.com/about/#why

この記事では、

  • 私が公式ドキュメントを読んでもすぐに理解できなかったAssertionメソッド
  • 私が「へー、こんなのもあるんだなー」と感じたAssertionメソッド

を中心に整理していきます。

Fluent Assertionsをインストールする

$ dotnet add package FluentAssertions

以後、.NETFluentAssertionsのバージョンは以下のとおりです。

myCsharpApp.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="FluentAssertions" Version="7.0.0" />
  </ItemGroup>

</Project>

Nullable

https://fluentassertions.com/nullabletypes/

HaveValue / NotBeNull

値を持っているかを検証するのがHaveValuenullでないことを検証するのがNotBeNullです。

int? theInt = 3;
theInt.Should().HaveValue(); // パスする
theInt.Should().NotBeNull(); // パスする
int? theInt = null;
theInt.Should().NotHaveValue(); // パスする
theInt.Should().BeNull(); // パスする

default値が設定された場合、HaveValueNotBeNullも値なしの扱いとなり、テストが失敗します。default値が指定された場合、NotBeNullは成功しそうな気もしますが、NotBeNullもテストは通りません。

int? theInt = default;
// 失敗 Expected a value.
theInt.Should().HaveValue();
// 失敗 Expected a value.
theInt.Should().NotBeNull();

HaveValueとNotBeNullの違い

HaveValueNotBeNullの主な違いは、対象型 と 使用目的 です。

HaveValueNullable<T>型に限定されますが、NotBeNullNullable<T>型に加えて参照型にも使えます。

string theString = "aaa";
int? theNullableInt = 33;
// ⭕️ NotBeNullは、参照型で使用できる
theString.Should().NotBeNull();
// ⭕️ HaveValueは、Nullable<T>で使用できる
theNullableInt.Should().HaveValue();
// ❌ HaveValueは、参照型では使用できない
theString.Should().HaveValue();
  • NotBeNull: 値がnullでないことを確認するときに使う
  • HaveValue: 値を持っていることを確認するときに使う

実際のところ、HaveValueNotBeNullの挙動は全く一緒ですが、検証意図に応じてHaveValueNotBeNullを使い分けることで、テストの可読性向上に繋がります。

文字列

https://fluentassertions.com/strings/

BeEmpty / BeNullOrWhiteSpace

BeEmpty(NotBeEmpty)は空文字とnullを明確に区別します。

string theString = "";
string theString2 = String.Empty;
string theString3 = null!;

// パスする
theString.Should().BeEmpty();
// パスする
theString2.Should().BeEmpty();

// 失敗 Expected theString3 to be empty, but found <null>.
// BeEmptyは空文字を期待し、nullを許容しないため失敗
theString3.Should().BeEmpty();
// パスする
theString3.Should().NotBeEmpty();

nullと空文字をセットでチェックしたい場合はBeNullOrWhiteSpace(NotBeNullOrWhiteSpace)が使えます。

string theString = "";
string theString2 = String.Empty;
string theString3 = null!;

// パスする
theString.Should().BeNullOrWhiteSpace();
// パスする
theString2.Should().BeNullOrWhiteSpace();
// パスする
theString3.Should().BeNullOrWhiteSpace();

// 失敗 Expected theString not to be <null> or whitespace, but found "".
theString.Should().NotBeNullOrWhiteSpace();
// 失敗 Expected theString not to be <null> or whitespace, but found "".
theString2.Should().NotBeNullOrWhiteSpace();
// 失敗 Expected theString3 not to be <null> or whitespace, but found <null>.
theString3.Should().NotBeNullOrWhiteSpace();

Be / BeEquivalentTo

Beは大文字小文字を区別して比較します。一方で、BeEquivalentToは大文字小文字を区別せずに比較します。

string theString = "This is a String";

// 失敗 Expected theString to be "THIS IS A STRING", but "This is a String" differs near "his" (index 1).
theString.Should().Be("THIS IS A STRING");
// パスする
theString.Should().BeEquivalentTo("THIS IS A STRING");

Contain / ContainEquivalentOf

ContainEquivalentOfも同様に大文字小文字を区別せずに比較します。また、Contain系のAssertionメソッドは、対象文字列が含まれる回数まで検証可能です。

string theString = "This is a String";

theString.Should().Contain("is a"); // パスする
theString.Should().Contain("is a", Exactly.Once()); // パスする
// 失敗 Expected theString "This is a String" to contain "is a" at least 2 times, but found it 1 time.
theString.Should().Contain("is a", AtLeast.Twice());

theString.Should().ContainEquivalentOf("IS A"); // パスする

Collections

https://fluentassertions.com/collections/

NotBeEmpty / NotBeNullOrEmpty

Collectionが存在することの確認は、NotBeEmptyNotBeNullOrEmptyが使えます。いずれも、nullの場合に失敗します。挙動は同じですが、コレクションがnullの可能性がある場合はNotBeNullOrEmptyを使い、nullを想定しない場合はNotBeEmptyを使うのが良いと思います。これによって、何を検証するかの意図が明確になります。

IEnumerable<int> collection1 = new[] { 1, 2, 5, 8 };
collection1.Should().NotBeEmpty(); // パス
collection1.Should().NotBeNullOrEmpty(); // パス

IEnumerable<int> collection2 = new List<int>();
// 失敗 Expected collection2 not to be empty.
collection2.Should().NotBeEmpty();
// 失敗 Expected collection2 not to be empty.
collection2.Should().NotBeNullOrEmpty();

IEnumerable<int> collection3 = null!;
// 失敗 Expected collection3 not to be empty, but found <null>.
collection3.Should().NotBeEmpty();
// 失敗 Expected collection3 not to be <null>.
collection3.Should().NotBeNullOrEmpty();

Equal / BeEquivalentTo

次は、EqualBeEquivalentToです。いずれもコレクションの要素一致を検証するメソッドとなります。

使い分けとしては、

  • 順序まで厳密に比較したい場合はEqual
  • 順序関係なく要素が一致のみで良い場合はBeEquivalentTo

とすれば良さそうです。

IEnumerable<int> collection = new[] { 1, 2, 5, 8 };

collection.Should().Equal(new List<int> { 1, 2, 5, 8 }); // パスする
collection.Should().Equal(1, 2, 5, 8); // パスする
// BeEquivalentToは順序関係なく要素が一致しているかを検証する
collection.Should().BeEquivalentTo(new[] {8, 2, 1, 5}); // パスする
// BeEquivalentToには順序の厳密比較をするオプションがある
collection.Should().BeEquivalentTo(new List<int> {8, 2, 1, 5}, options => options.WithStrictOrdering());
// 失敗 Expected collection[0] to be 8, but found 1. Expected collection[2] to be 1, but found 5. Expected collection[3] to be 5, but found 8.

HaveCount

要素数の検証にはHaveCountが使えます。

HaveCount以外はそこまで使用頻度は多くなさそうですが、HaveSameCountは存在自体初めて知ったので載せておきます。

IEnumerable<int> collection = new[] { 1, 2, 5, 8 };

collection.Should().HaveCount(4); // パスする
collection.Should().HaveCountGreaterThan(3); // パスする
collection.Should().HaveCountGreaterThanOrEqualTo(4); // パスする
// 指定のコレクションと要素数が同じかを検証する
collection.Should().HaveSameCount(new[] { 6, 2, 0, 5 }); // パスする

BeSubsetOf

BeSubsetOfは、コレクションが指定されたスーパーセットの部分集合であることを検証するメソッドです。

IEnumerable<int> collection1 = new[] { 1, 2, 5, 8 };
IEnumerable<int> collection2 = new[] { 1, 2, 5, 98 };
// パスする
collection1.Should().BeSubsetOf(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, });
// 失敗 Expected collection2 to be a subset of {1, 2, 3, 4, 5, 6, 7, 8, 9}, but items {98} are not part of the superset.
collection2.Should().BeSubsetOf(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, });

Contain

コレクションのContainです。Containには単一の要素だけではなく、別のコレクションを渡すことも可能です。

IEnumerable<int> collection = new[] { 1, 2, 5, 8 };
// 5を含むのでパス
collection.Should().Contain(5);
// 2,5を含むのでパス
collection.Should().Contain(new[] { 2, 5 });
// 失敗 Expected collection {1, 2, 5, 8} to contain {2, 5, 9}, but could not find {9}.
collection.Should().Contain(new[] { 2, 5, 9 });

Contain(条件式)は、検証対象のコレクションに最低1つでも条件に合致する要素があるかを検証します。

IEnumerable<int> collection1 = new[] { 1, 2, 5, 8 };
// 3よりも大きい要素が存在するため、パスする
collection1.Should().Contain(x => x > 3);
// 失敗 Expected collection {1, 2, 5, 8} to have an item matching (x > 100).
collection1.Should().Contain(x => x > 100);

ContainSingle

ContainSingleは、要素が1つであることを検証するメソッドです。

IEnumerable<int> collection1 = new[] { 4 };
IEnumerable<int> collection2 = new[] { 4, 5 };
// 要素数が 1 であることを検証
// パスする
collection1.Should().ContainSingle();
// 失敗 Expected collection2 to contain a single item, but found {4, 5}.
collection2.Should().ContainSingle();

Contain同様に条件式を指定し、より詳細に要素を検証することも可能です。

IEnumerable<string> collection3 = new[] { "test" };
IEnumerable<string> collection4 = new[] { "test", "test" };
// 要素数が1つであることとその値についての検証
// パスする
collection3.Should().ContainSingle(x => x.Contains("test"));
// 失敗 Expected collection4 to contain a single item matching x.Contains("test"), but 2 such items were found.
collection4.Should().ContainSingle(x => x.Contains("test"));

OnlyContain

OnlyContainは、コレクション内のすべての要素が指定された条件を満たすことを検証するメソッドです。

IEnumerable<int> collection1 = new[] { 1, 2, 5, 8 };
IEnumerable<int> collection2 = new[] { 1, 2, 5, 8, 98, 100 };
// 全要素が10より小さいため、パスする
collection1.Should().OnlyContain(x => x < 10);
// 失敗 Expected collection2 to contain only items matching (x < 10), but {98, 100} do(es) not match.
collection2.Should().OnlyContain(x => x < 10);

ContainInOrder

ContainInOrderは、指定した要素が順序を保った状態でコレクション内に並んでいることを検証します。この際、指定した要素は連続で並んでいる必要はありません。飛び飛びでも要素の順番が合っていれば検証は成功します。

一方で、ContainInConsecutiveOrderは、指定した要素が連続で順序通りに並んでいるかを検証します。

var collection1 = new[] { 1, 3, 5, 7, 8 };
// パスする
collection1.Should().ContainInOrder(new[] { 1, 5, 8 });
// Expected collection1 {1, 3, 5, 7, 8} to contain items {3, 1, 5} in order, but 1 (index 1) did not appear (in the right order).
collection1.Should().ContainInOrder(new[] { 3, 1, 5 });

var collection2 = new List<int> { 1, 2, 5, 8 };
// パスする
collection2.Should().ContainInConsecutiveOrder(new[] { 2, 5, 8 });
// Expected collection2 {1, 2, 5, 8} to contain items {1, 5, 8} in order, but 5 (index 1) did not appear (in the right consecutive order).
collection2.Should().ContainInConsecutiveOrder(new[] { 1, 5, 8 });

IntersectWith

IntersectWithは、共通要素が存在するかを検証します。

var collection = new[] { 1, 2, 5, 8 };
// 共通要素である1が存在するため、パスする
collection.Should().IntersectWith(new[] { 1, 3, 4 });
// Expected collection to intersect with {0, 3, 4}, but {1, 2, 5, 8} does not contain any shared items.
collection.Should().IntersectWith(new[] { 0, 3, 4 });

Enum

https://fluentassertions.com/enums/

Be / BeOneOf

enum MyEnum { One = 1, Two = 2, Three = 3}

var myEnum = MyEnum.One;
myEnum.Should().Be(MyEnum.One);
myEnum.Should().NotBe(MyEnum.Two);
myEnum.Should().BeOneOf(MyEnum.One, MyEnum.Two);

HaveSameNameAs / HaveSameValueAs

異なる型の名前や値を検証したい場合は、HaveSameNameAsHaveSameValueAsが使えます。

enum MyEnum { One = 1, Two = 2, Three = 3}
enum SameNameEnum { One = 11 }
enum SameValueEnum { OneOne = 1 }

var myEnumOne = MyEnum.One;
// "One"という名称が同一なのでパスする
myEnumOne.Should().HaveSameNameAs(SameNameEnum.One);
// 1という値が同一なのでパスする
myEnumOne.Should().HaveSameValueAs(SameValueEnum.OneOne);

HaveValue

定義されている数値自体を検証するメソッドとして、HaveValueがあります。

MyEnum.One.Should().HaveValue(1);
MyEnum.One.Should().NotHaveValue(2);

その他

Fluent Assertionsは、失敗した時に分かりやすく失敗原因を提示してくれます。

しかし、各Assertionbecause引数を指定することで、さらに失敗原因を分かりやすくすることも可能です。

// 失敗 Expected collection to be equal to {1, 2, 5, 8, 9} because 一致しない理由, but {1, 2, 5, 8} contains 1 item(s) less.
collection.Should().Equal(new List<int> { 1, 2, 5, 8, 9 }, "一致しない理由");

// 失敗 Expected collection to contain 3 item(s) because 総数不一致, but found 4: {1, 2, 5, 8}.
collection.Should().HaveCount(3, "総数不一致");

最後に

以上です。

Fluent Assertionsを使っている割には、知らないメソッドが沢山ありました。必要なタイミングで積極的に使っていきたいですね。

Discussion