【C#, NUnit】NUnit で自前のクラスを比較するにはどうすればいいのかいろいろ調べたことのまとめ【IEquatable】

🙇‍♂️解釈違いなどがあればご指摘ください。
🙇‍♂️いずれも参考資料が詳しくそちらを参照していただくのみで充分な内容であるかもしれません。
🙇‍♂️同じような疑問からスタートされた方に対してどのような手順で調査して理解が得られたのかということを共有したいと思います。

**※コメントでのご指摘を受けて本文中の `Assert.That` による比較という表現を `EqualConstraint` による比較に修正しました。**
**※さらに追記でタイトルの修正もしました**

# 調べたこと
下記のようなクラス `TestClass1`に対しての値の検証 `Test1` の結果が失敗❌するのは何故なのかを調べました。
最終的には `Test1` を成功✅させるにはどうしたらよいのかということまでまとめます。

```c#:TestClass1
public class TestClass1
{
    public string Id { get; set; }
    public string Name { get; set; }

    public TestClass1(string id, string name)
    {
        Id = id;
        Name = name;
    }
}
```

```c#:Test1
[Test]
public void Test1()
{
    TestClass1 actual = new TestClass1("1", "takashi");
    TestClass1 expect = new TestClass1("1", "takashi");
    Assert.That(actual, Is.EqualTo(expect)); //失敗する
}
```

# 環境など
+ Visual Studio 2019
+ Microsoft.NET.Test.Sdk 16.5.0
+ NUnit 3.12.0
+ NUnit3TestAdapter 3.16.1

# 等価性について
なんとなくの知識として、オブジェクト同士で比較する場合にはそれらが同一のインスタンスであるかどうかでイコールかそうでないかが決まるというような理解でした。つまり下記のようになるということは知っていました。

```c#
TestClass1 actual = new TestClass1("1", "takashi");
TestClass1 expect = new TestClass1("1", "takashi");
var isEqual = actual == expect // False
```

ですので、~~`Assert.That`~~ `EqualConstraint (Is.EqualTo)` による比較においてもこれと同じ要領で比較が行われた結果、テストは失敗したのだと考え、まずは等値演算子(==)と「等価性」について調べてみました。

まず、等価性を考えるにあたって、その対象には「値型」と「参照型」とがあり（リファレンスには他に「文字列」と「デリゲート」がありました）今回作成した `TestClass1` のようなクラスは**参照型となります**。等価性を求めるためには等値演算子と `Equals` メソッドが用いられ、参照型は既定ではそれらにおいて「参照の等価性」の比較を行っています。また、ここでの既定の `Equals` メソッドとは [Object.Equals メソッド](https://docs.microsoft.com/ja-jp/dotnet/api/system.object.equals?view=netcore-3.1) であると理解しました。

ここまでで、私が求めている二つのオブジェクトの比較は「値の等価性」を求めるものであり ~~`Assert.That`~~ `EqualConstraint` は「参照の等価性」の比較を行っているのではないかということがわかりました。

>
[参照型の等価性](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/equality-operators#reference-types-equality)
[==演算子とEqualsメソッドの違いとは？［C#］](https://www.atmarkit.co.jp/ait/articles/1802/28/news028.html)

# EqualConstraint について
※章題変更しました。

では、~~`Assert.That`~~ `EqualConstraint` による比較は参照の等価性の比較なのかということを調べてみました。
[https://github.com/nunit/docs/wiki/EqualConstraint#notes](https://github.com/nunit/docs/wiki/EqualConstraint#notes) にそのようなことが書いてありました。
> When checking the equality of user-defined classes, NUnit first examines each class to determine whether it implements IEquatable<T> (unless the AsCollection modifier is used). If either object implements the interface for the type of the other object, then that implementation is used in making the comparison. If neither class implements the appropriate interface, NUnit makes use of the Equals override on the expected object. If you neglect to either implement IEquatable<T> or to override Equals, you can expect failures comparing non-identical objects. In particular, overriding operator == without overriding Equals or implementing the interface has no effect.

クラスに `IEquatable <T>` の実装がなければオーバーライドされた `Equals` メソッドを、`Equals` メソッドのオーバーライドがなければ既定の `Object.Equals` を用いて等価性の検証をするということでしょうか🤔
であれば、`TestClass1` にはいずれもありませんから参照の等価性で比較を行った結果テストが失敗❌したのだと考えられます。

> [https://github.com/nunit/docs/wiki/EqualConstraint](https://github.com/nunit/docs/wiki/EqualConstraint)

# IEquatable の実装

上記にて `IEquatable` というものについて言及されていたのでこちらについて調べました。
とはいえ [IEquatableを完全に理解する](https://qiita.com/cactuaroid/items/f277a097ecf51eb247c0) という記事でまとめられており自分が改めてまとめる必要はないかと思います。
ただ、ここまで調べてきて「可変型（ミュータブル）」についてまだうまく理解しきれていません。「値」であるかという点に注目して等価性判定についてどのように実装するのかということはなんとなくわかったので、参照型である自前のクラスが値としての性質を持っている場合には不変型（イミュータブル）とするべきであると考えると良いのでしょうか🤔今回の実際のケース（※後述）においては「値」で「イミュータブル」なクラスと判断したので `IEquatable` を実装しました（自動生成）。

参考記事でも紹介されていましたが Visual Studio の `クイック アクションとリファクタリング` の機能でクラスに対して `IEquatable` の実装の[自動生成ができます（Visual Studio 2019版）](https://docs.microsoft.com/ja-jp/visualstudio/ide/reference/generate-equals-gethashcode-methods?view=vs-2019)。また、同記事 [配列等に注意](https://qiita.com/cactuaroid/items/f277a097ecf51eb247c0#%E9%85%8D%E5%88%97%E7%AD%89%E3%81%AB%E6%B3%A8%E6%84%8F) は実際に嵌っていたところを解決してくれました。

>
[IEquatableを完全に理解する](https://qiita.com/cactuaroid/items/f277a097ecf51eb247c0)
[Visual Studio で Equals および GetHashCode メソッドのオーバーライドを生成する](https://docs.microsoft.com/ja-jp/visualstudio/ide/reference/generate-equals-gethashcode-methods?view=vs-2019)
[値型と参照型とミュータブルとイミュータブルと](https://qiita.com/chocolamint/items/020143cf249505bf2ffc)

# まとめ
今回の疑問のスタート地点はテストがなぜ失敗するのかというところであり ~~`Assert.That`~~ `EqualConstraint ` の挙動から `IEquatable`に辿り着きました。しかし、本来は等価性という考え方に基づいて `IEquatable` や等値演算子の実装のガイドラインやベストプラクティスが存在し ~~`Assert.That`~~ `EqualConstraint` においてもそちらに基づいているのだと理解しました。

自前のクラスにおいてもそうしたガイドラインに従い等価性判定のメソッドを実装する必要があります。今回のケースではその一つのパターンとして `IEquatable` の実装及び `Equals` メソッド、`GetHashCode` メソッドのオーバーライド、等値演算子のオーバーロードを行いました。

まだ完全に理解できていない部分、調査できていない部分あると思います。そうした部分については今後実際のケースを通して今回のように調査しながら深めていけたらと思っています。

```c#:TestClass1(IEquatableを実装)
public class TestClass1 : IEquatable<TestClass1>
{
    public string Id { get; }
    public string Name { get; }
    public TestClass1(string id, string name)
    {
        Id = id;
        Name = name;
    }
    public override bool Equals(object obj)
    {
        return Equals(obj as TestClass1);
    }
    public bool Equals([AllowNull] TestClass1 other)
    {
        return other != null &&
               Id == other.Id &&
               Name == other.Name;
    }
    public override int GetHashCode()
    {
        return HashCode.Combine(Id, Name);
    }
    public static bool operator ==(TestClass1 left, TestClass1 right)
    {
        return EqualityComparer<TestClass1>.Default.Equals(left, right);
    }
    public static bool operator !=(TestClass1 left, TestClass1 right)
    {
        return !(left == right);
    }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174132/11084563-f662-2640-213f-da0a1b92ebac.png)
Test1 成功の図

# 補足など
実際のケースは、API層(Controller)、ドメイン層、データ層のような構成の Web API におけるドメイン層での EntityModel Class のデータを ViewModel Class のデータに変換するロジック部分のメソッドの検証です。

また、[https://stackoverflow.com/questions/318210/compare-equality-between-two-objects-in-nunit](https://stackoverflow.com/questions/318210/compare-equality-between-two-objects-in-nunit) でテストのためだけに `Equals` メソッドのオーバーライドなどはするべきではないという回答がありました。これについては、ドメインロジックに影響するから～というよりかは、等価性に関する検討を行い適切なパターンで等価性判定を実装してそれに従ってテストしなくてはいけないから（あるいはテストのほうが先にできている）と考える方が、今回調査してみた結果、自分なりにしっくりきます。

# 参考
+ [参照型の等価性](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/equality-operators#reference-types-equality)
+ [==演算子とEqualsメソッドの違いとは？［C#］](https://www.atmarkit.co.jp/ait/articles/1802/28/news028.html)
+ [https://github.com/nunit/docs/wiki/EqualConstraint](https://github.com/nunit/docs/wiki/EqualConstraint)
+ [IEquatableを完全に理解する](https://qiita.com/cactuaroid/items/f277a097ecf51eb247c0)
+ [Visual Studio で Equals および GetHashCode メソッドのオーバーライドを生成する](https://docs.microsoft.com/ja-jp/visualstudio/ide/reference/generate-equals-gethashcode-methods?view=vs-2019)
+ [値型と参照型とミュータブルとイミュータブルと](https://qiita.com/chocolamint/items/020143cf249505bf2ffc)
+ [https://stackoverflow.com/questions/318210/compare-equality-between-two-objects-in-nunit](https://stackoverflow.com/questions/318210/compare-equality-between-two-objects-in-nunit)