本記事は C# でテストを記述する人を対象に、しばしば複合型の等値アサートで生じる「面倒さ」に対するソリューションの一つを紹介します。テストフレームワークはなんでも。
その前に、先ずはシンプルな (そして理想的な) 等値アサートから:
public string GetFoo() {
return "Foo";
}
...
string actual = GetFoo();
string expected = "Foo";
Assert.AreEqual(expected, actual); // 成功!
アサート対象が int
や string
といった基本型なら、たいてい困る事もなく素直に書けます。
しかし、対象がユーザー定義型のような複合型だとそうは問屋が卸さない。
面倒な等値アサート
例えば Account
というユーザー定義クラスと、それを返すクエリ サービス AccountQuery
があったとします:
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
が期待通りか調べるテストを書いてみます:
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 に与える事でフォローできる場合もありますが、あまりにも自明な等値アサートをしたいだけなのに新たに型を実装するとか面倒すぎます。
このように、等値アサートの書き方に時間を取られるのはもうウンザリ
PrimitiveAssert で簡潔に書く
PrimitiveAssert はテスト対象データをいくつかの基本型(プリミティブ データ)に分解し、個別に比較します。
API は基本的に次の拡張メソッド一つだけです。
actual.AssertIs(expected);
これで書き換えてみましょう:
Account actual = new AccountQuery().Find(id: 1);
Account expected = new Account { Id = 1, Name = "Foo1", Tags = { "tag1" } };
actual.AssertIs(expected); // 成功!
今度はいい感じに等値アサートしてくれます! 見た目もシンプルになりました!!
データメンバーにコレクションがあっても要素に分解して等値アサートします。
API も一つだけなので、基本的にはこれだけです。
そう、これぐらい簡単でいいんだよ
もう少し詳解
コレクションの等値アサートはできる?
Yes!
汎用的なコレクション (System.Collections
以下にあるような型) 同士であれば、型に関わらずコレクションとして等値アサートされます。
List<string> actual = new List<string> { "foo", "bar" };
string[] expected = new[] { "foo", "bar" };
actual.AssertIs(expected); // 成功!
タプルの等値アサートはできる?
Yes!
Tuple
同士、ValueTuple
同士はもちろん、Tuple
と ValueTuple
の等値アサートでも問題ありません。
var actual = Tuple.Create("foo", "bar");
var expected = ("foo", "bar");
actual.AssertIs(expected); // 成功!
循環参照の等値アサートはできる?
Yes!
ウロボロスに囚われる事はありません。
class Foo {
public Foo Self => this;
}
...
Foo actual = new Foo();
Foo expected = new Foo();
actual.AssertIs(expected); // 成功!
本当に全てのデータメンバーが等値アサートされている?
という場合は、コンソールにログを出力してみることもできます。
// AssertIs() を呼び出す前、テストのセットアップ処理とかに書いておく。
// 既定値は false
PrimitiveAssert.ConsoleLogging = true;
だいたい下記のような出力が得られます。
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
)が設定されます。
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
が無いと失敗するようにもなりました。これでデータメンバーがアサートから漏れている状況を検知する事ができます。
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 には PrimitiveAssert の詳細とより多くの事例が載っています。ぜひ参照してみてください。
つまり?
PrimitiveAssert を使えばたいていの等値アサートは簡潔に書けます!
var actual = new { Foo = "Foo1", Bar = new List<int>{ 1, 2 } };
actual.AssertIs(new { Foo = "Foo1", Bar = new[] { 1, 2 } }); // 成功!