面倒な等値アサートを簡潔に書く

本記事は C# でテストを記述する人を対象に、しばしば**複合型の等値アサートで生じる「面倒さ」**に対するソリューションの一つを紹介します。テストフレームワークはなんでも。

その前に、先ずはシンプルな (そして理想的な) 等値アサートから：

```cs
public string GetFoo() {
    return "Foo";
}

...

string actual = GetFoo();
string expected = "Foo";

Assert.AreEqual(expected, actual);  // 成功！
```

アサート対象が `int` や `string` といった基本型なら、たいてい困る事もなく素直に書けます。
しかし、対象がユーザー定義型のような**複合型**だとそうは問屋が卸さない。

## 面倒な等値アサート

例えば `Account` というユーザー定義クラスと、それを返すクエリ サービス `AccountQuery` があったとします：

```cs
class Account {
  public int Id { get; set; }
  public string Name { get; set; }
  public List<string> Tags { get; } = new List<string>();
}

class AccountQuery {
  /// 例なので固定データを返すだけ。
  public Account Find(int id) {
    return new Account { Id = id, Name = "Foo" + id, Tags = { "tag1" } };
  }
}
```

次に、`AccountQuery` が返す `Account` が期待通りか調べるテストを書いてみます：

```cs
Account actual = new AccountQuery().Find(id: 1);
Account expected = new Account { Id = 1, Name = "Foo1", Tags = { "tag1" } };

//Assert.AreEqual(expected, actual);  // 大抵のテストフレームワークで参照比較となり失敗する。ので、
Assert.AreEqual(expected.Id, actual.Id);  // データメンバー毎に等値アサートが必要
Assert.AreEqual(expected.Name, actual.Name);
CollectionAssert.AreEqual(expected.Tags, actual.Tags);  // コレクションには専用の Assert が必要な事が殆ど
// Account に Enabled プロパティが足されたらアサート忘れそう・・・
```

等値アサートしたい actual は一つだけなのに、わざわざデータメンバーの数だけ `Assert.AreEqual()` を書くのは大変です。しかもデータメンバーの型に応じて専用の Assert メソッドを使い分ける必要もあったり。 ~~`Assert.AreEqual()` で全部いい感じにやってくれYO~~

更に問題なのが、`Account` に新しいプロパティが追加された場合でも、上記のテストは**追加プロパティをアサートする事なく成功してしまう**という事。これはマズイ。

テストフレームワークによっては `IEuqalityComparer<T>` 等のカスタム等値性を Assert に与える事でフォローできる場合もありますが、あまりにも自明な等値アサートをしたいだけなのに新たに型を実装するとか面倒すぎます。

このように、等値アサートの**書き方に時間を取られる**のはもうウンザリ :confounded:


## PrimitiveAssert で簡潔に書く

https://www.nuget.org/packages/Inasync.PrimitiveAssert/

*PrimitiveAssert* はテスト対象データをいくつかの基本型（プリミティブ データ）に分解し、個別に比較します。
API は基本的に次の拡張メソッド一つだけです。

```cs
actual.AssertIs(expected);
```

これで書き換えてみましょう：

```cs
Account actual = new AccountQuery().Find(id: 1);
Account expected = new Account { Id = 1, Name = "Foo1", Tags = { "tag1" } };

actual.AssertIs(expected);  // 成功！
```

今度はいい感じに等値アサートしてくれます！ 見た目もシンプルになりました！！
データメンバーにコレクションがあっても要素に分解して等値アサートします。

API も一つだけなので、基本的にはこれだけです。
そう、これぐらい簡単でいいんだよ :sob:


## もう少し詳解
### コレクションの等値アサートはできる？
Yes!
汎用的なコレクション (`System.Collections` 以下にあるような型) 同士であれば、型に関わらずコレクションとして等値アサートされます。

```cs
List<string> actual = new List<string> { "foo", "bar" };
string[] expected = new[] { "foo", "bar" };

actual.AssertIs(expected);  // 成功！
```

### タプルの等値アサートはできる？
Yes!
`Tuple` 同士、`ValueTuple` 同士はもちろん、`Tuple` と `ValueTuple` の等値アサートでも問題ありません。

```cs
var actual = Tuple.Create("foo", "bar");
var expected = ("foo", "bar");

actual.AssertIs(expected);  // 成功！
```

### 循環参照の等値アサートはできる？
Yes!
ウロボロスに囚われる事はありません。

```cs
class Foo {
    public Foo Self => this;
}

...

Foo actual = new Foo();
Foo expected = new Foo();

actual.AssertIs(expected);  // 成功！
```

### 本当に全てのデータメンバーが等値アサートされている？
という場合は、コンソールにログを出力してみることもできます。

```cs
// AssertIs() を呼び出す前、テストのセットアップ処理とかに書いておく。
// 既定値は false
PrimitiveAssert.ConsoleLogging = true;
```

だいたい下記のような出力が得られます。

```cs:Console
actual: Account = {
    Id: Int32 = 1    // actual と expected は数値型として等しいです。
    Name: String = "Foo1"    // actual と expected は String 型として等しいです。
    Tags: List<String> = {
        [0]: String = "tag1"    // actual と expected は String 型として等しいです。
    }
}
```

### expected は匿名型でも良い？
Yes!
*PrimitiveAssert* は基本的に型を比較しません。代わりにターゲット型として指定した型のデータメンバーを全て満たしているか否かを検証します。その為、expected は匿名型でも、全く関係のない HogeHoge 型でも問題ありません。

また、ターゲット型の指定は任意で、省略時には actual の型（ここでは `Account`）が設定されます。

```cs
Account actual = new AccountQuery().Find(id: 1);
var expected = new { Id = 1, Name = "Foo1", Tags = new[] { "tag1" } };

actual.AssertIs(expected);  // 成功！
actual.AssertIs<Account>(expected);  // 型パラメーターでターゲット型を指定できる
```

#### expected にはターゲット型の全てのデータメンバーが必要

前述の通り、expected は型に縛られない代わりに、ターゲット型の全てのデータメンバーを満たす必要があります。
これにより、後々 `Account` に `Enabled` プロパティが追加された場合、expected にも `Enabled` が無いと失敗するようにもなりました。これで**データメンバーがアサートから漏れている状況を検知**する事ができます。

```cs
class Account {
  ...
  public bool Enabled { get; set; }  // 新たに追加したプロパティ
}

class AccountQuery {
  public Account Find(int id) {
    return new Account { Id = id, Name = "Foo" + id, Tags = { "tag1" }, Enabled = true };
  }
}

...

var actual = new AccountQuery().Find(id: 1);
//actual.AssertIs(new { Id = 1, Name = "Foo1", Tags = new[] { "tag1" } });  // Enabled が無いので失敗
actual.AssertIs(new { Id = 1, Name = "Foo1", Tags = new[] { "tag1" }, Enabled = true });  // 成功！
```


### More info
[README.md](https://github.com/in-async/PrimitiveAssert/blob/master/README.md) には *PrimitiveAssert* の詳細とより多くの事例が載っています。ぜひ参照してみてください。

## つまり？
[PrimitiveAssert](https://github.com/in-async/PrimitiveAssert) を使えばたいていの等値アサートは簡潔に書けます！

```cs
var actual = new { Foo = "Foo1", Bar = new List<int>{ 1, 2 } };

actual.AssertIs(new { Foo = "Foo1", Bar = new[] { 1, 2 } });  // 成功！
```

## 参照
* GitHub: https://github.com/in-async/PrimitiveAssert
* Nuget: https://www.nuget.org/packages/Inasync.PrimitiveAssert/