Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9517

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

$
0
0

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

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

publicstringGetFoo(){return"Foo";}...stringactual=GetFoo();stringexpected="Foo";Assert.AreEqual(expected,actual);// 成功!

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

面倒な等値アサート

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

classAccount{publicintId{get;set;}publicstringName{get;set;}publicList<string>Tags{get;}=newList<string>();}classAccountQuery{/// 例なので固定データを返すだけ。publicAccountFind(intid){returnnewAccount{Id=id,Name="Foo"+id,Tags={"tag1"}};}}

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

Accountactual=newAccountQuery().Find(id:1);Accountexpected=newAccount{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 は基本的に次の拡張メソッド一つだけです。

actual.AssertIs(expected);

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

Accountactual=newAccountQuery().Find(id:1);Accountexpected=newAccount{Id=1,Name="Foo1",Tags={"tag1"}};actual.AssertIs(expected);// 成功!

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

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

もう少し詳解

コレクションの等値アサートはできる?

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

List<string>actual=newList<string>{"foo","bar"};string[]expected=new[]{"foo","bar"};actual.AssertIs(expected);// 成功!

タプルの等値アサートはできる?

Yes!
Tuple同士、ValueTuple同士はもちろん、TupleValueTupleの等値アサートでも問題ありません。

varactual=Tuple.Create("foo","bar");varexpected=("foo","bar");actual.AssertIs(expected);// 成功!

循環参照の等値アサートはできる?

Yes!
ウロボロスに囚われる事はありません。

classFoo{publicFooSelf=>this;}...Fooactual=newFoo();Fooexpected=newFoo();actual.AssertIs(expected);// 成功!

本当に全てのデータメンバーが等値アサートされている?

という場合は、コンソールにログを出力してみることもできます。

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

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

Console
actual と expected は数値型として等しいです。
{
      path: ./Id:Int32
    target: System.Int32
    actual: 1
  expected: 1
}
actual と expected は String 型として等しいです。
{
      path: ./Name:String
    target: System.String
    actual: Foo1
  expected: Foo1
}
actual と expected は String 型として等しいです。
{
      path: ./Tags:List`1/0:String
    target: System.String
    actual: tag1
  expected: tag1
}

expected は匿名型でも良い?

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

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

Accountactual=newAccountQuery().Find(id:1);varexpected=new{Id=1,Name="Foo1",Tags=new[]{"tag1"}};actual.AssertIs(expected);// 成功!actual.AssertIs<Account>(expected);// 型パラメーターでターゲット型を指定できる

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

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

classAccount{...publicboolEnabled{get;set;}// 新たに追加したプロパティ}classAccountQuery{publicAccountFind(intid){returnnewAccount{Id=id,Name="Foo"+id,Tags={"tag1"},Enabled=true};}}...varactual=newAccountQuery().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を使えばたいていの等値アサートは簡潔に書けます!

varactual=new{Foo="Foo1",Bar=newList<int>{1,2}};actual.AssertIs(new{Foo="Foo1",Bar=new[]{1,2}});// 成功!

参照


Viewing all articles
Browse latest Browse all 9517

Trending Articles