goal
XUnitを使ったテストにおいて、MemberDataを使い、データドリブンテストを行ったとき、Testエクスプローラーで1件のテストとして認識されるのを何とかしたい。
つまり、データごとのテストとして認識されたい。
前提
テストデータとして、複雑なオブジェクトを想定する。
プリミティブ型の場合、標準の方法でやりたいことが実現できているので、そのケースは対象外とする。
テストデータの実装例は以下の通り。
public class TestData { public int IntProp { get; set; } public string StringProp { get; set; } public InnerTestData MyProperty { get; set; } = new InnerTestData(); public class InnerTestData { public int IntProp { get; set; } public string StringProp { get; set; } } }
パターン1
ポイント
基本的な使い方に従い作成した。
実装例
public static IEnumerable<object[]> InData { get { yield return new object[] { new TestData(), "expected" }; yield return new object[] { new TestData(), "expected" }; } } [Theory] [MemberData(nameof(InData))] public void Test1(TestData testData, string expected) { Assert.True(true); }
エクスプローラーの表示例
TestProject1.UnitTest1.Test1(testData: TestData { IntProp = 0, MyProperty = InnerTestData { IntProp = 0, StringProp = null }, StringProp = null }, expected: "expected") 成功 < 1 ミリ秒
実際のテストケースは2件あるが、1件のテストとしてまとめられた。こうなると、どちらか片方だけを指定してテストを流したい、Debugしたいと思ったときに、それができず不便。
パターン2
ポイント
stackoverflowを参考に、'MemberDataSerializer'クラスを定義し、それを使ったテストにした。
実装例
public static IEnumerable<object[]> TestData() { yield return new object[] { "test1", new MemberDataSerializer<TestData>(new TestData()) }; yield return new object[] { "test2", new MemberDataSerializer<TestData>(new TestData()) }; } [Theory] [MemberData(nameof(TestData))] public void Test2(string testName, MemberDataSerializer<TestData> testCase) { Assert.True(true); }
エクスプローラーの表示例
テスト グループ: TestProject1.UnitTest1.Test2 成功 (2) 6 ミリ秒 TestProject1.UnitTest1.Test2(testName: "test1", testCase: MemberDataSerializer`1 { Object = TestData { IntProp = 0, MyProperty = InnerTestData { ... }, StringProp = null } }) 成功 < 1 ミリ秒 TestProject1.UnitTest1.Test2(testName: "test2", testCase: MemberDataSerializer`1 { Object = TestData { IntProp = 0, MyProperty = InnerTestData { ... }, StringProp = null } }) 成功 6 ミリ秒
一つ一つのデータがテストケースとして認識されるようになった。
MemberDataSerializerクラスの定義が必要だが、それさえあればシンプルに実装ができる。
テストデータとして渡すオブジェクトが複雑になったとき、それだけではテストケースの概要をつかめなくなりそうなので、テストケース名か、テストの短い概要を渡すようにするとよいかもしれない。
パターン3
テストケースで利用するデータをDictionary型のコレクションに格納しておく。データドリブンテストとしては、それぞれのテストケースのKey値を定義し、使うようにする。
テストメソッドの中で、Keyを使ってテストデータや、期待値を取得することで、やりたいことを実現している。
実装例
public static Dictionary<string, object[]> TestCases = new Dictionary<string, object[]>() { { "TestCase3-1", new object[]{new TestData(),"Expected" } }, { "TestCase3-2", new object[]{new TestData(),"Expected" } } }; public static IEnumerable<Object[]> TestData3() { for (int i = 1; i <= TestCases.Count; i++) { yield return new object[] { $"TestCase3-{i}" }; } } [Theory] [MemberData(nameof(TestData3))] public void Test3(string testCase) { var testData = TestCases[testCase][0] as TestData; var expected = TestCases[testCase][1] as string; Assert.True(true); }
エクスプローラーの表示例
テスト グループ: TestProject1.UnitTest1.Test3 成功 (2) < 1 ミリ秒 TestProject1.UnitTest1.Test3(testCase: "TestCase3-1") 成功 < 1 ミリ秒 TestProject1.UnitTest1.Test3(testCase: "TestCase3-2") 成功 < 1 ミリ秒
このパターンでも、データごとにテストケースとして認識された。
パターン2に比べ、表示内容がシンプルになるので、場合によってはテストケースを見つけやすいかもしれない。
比較
- パターン1は論外。
- 実装量は、パターン2が少なくてよさそう。
- テストエクスプローラーの表示的には、シンプルな表示ができるパターン3もよいが、拡張性まで考えるとパターン2がよさそう。
- テストの複雑さについては、データのキャストなどの手間を省けるため、パターン2が優勢。
結論:パターン2を採用する。
参考
public class MemberDataSerializer<T> : IXunitSerializable where T : new() { public T Object { get; private set; } public MemberDataSerializer() { Object = new T(); } public MemberDataSerializer(T objectToSerialize) { Object = objectToSerialize; } public void Deserialize(IXunitSerializationInfo info) { if (info == null) throw new InvalidOperationException(); var data = info.GetValue<string>("objValue"); if (data == null) throw new InvalidOperationException(); var objectData = JsonConvert.DeserializeObject<T>(data); if (objectData == null) throw new InvalidOperationException(); Object = objectData; } public void Serialize(IXunitSerializationInfo info) { var json = JsonConvert.SerializeObject(Object); info.AddValue("objValue", json); } }