プログラムを中心とした個人的なメモ用のブログです。 タイトルは迷走中。
内容の保証はできませんのであしからずご了承ください。

2020/07/02

[C#] xUnit で MemberData を使ったテストがテストエクスプローラーで1つのテストとして表示される

event_note2020/07/02 5:04

xUnitMemberDataClassData を使ってテストケースを作成した場合、 Visual Studio のテストエクスプローラーでは複数のテストケースが単一のテストケースとして表示されてしまいます。

これだとデバッグが非常にやりにくいので、InlineData と同じように複数のテストケースとして表示する方法がないか調べてみると、以下の記事が見つかりました。

Visual Studio のテストエクスプローラーで各テストケースを別々の項目として表示するためには、テストケースの型に IXunitSerializable を実装する必要があるみたいです。

環境

  • Visual Studio 2017
  • .NET Core 2.2
  • xUnit 2.4.1

サンプル

変更前

例えば、MemberData を使ったテストコードは以下のような感じになっていると思います。

[Theory,
    MemberData(nameof(TestData))]
public void TestMethod(int param1, string param2)
{
    // Do something test
}

public static IEnumerable<object[]> TestData()
{
    yield return new object[] { 0, "hoge" };
    yield return new object[] { 1, "fuga" };
}

変更後

IXunitSerializable を実装するためにテストデータクラス TestCaseData を作成します。

[Theory,
    MemberData(nameof(TestData))]
public void TestMethod(TestCaseData testCaseData)
{
    var param1 = testCaseData.Param1;
    var param2 = testCaseData.Param2;
    
    // Do something test
}

public class TestCaseData : IXunitSerializable
{
    public int Param1 { get; set; }
    public string Param2 { get; set; }
    
    public void Serialize(IXunitSerializationInfo info)
    {
        info.AddValue(nameof(Param1), Param1.ToString());
        info.AddValue(nameof(Param2), Param2.ToString());
    }

    public void Deserialize(IXunitSerializationInfo info) { }
}

public static IEnumerable<object[]> TestData()
{
    yield return new object[] {
        new TestCaseData{
            Param1 = 0,
            Param2 = "hoge"
        }
    };
    yield return new object[] {
        new TestCaseData{
            Param1 = 1,
            Param2 = "fuga"
        }
    };
}

結構面倒です。

もう少し汎用的になるように改良してみる

上記のままだと、テストケースの度に IXunitSerializable を実装する必要があり面倒なので、何とかして共通化できないかと思い、とりあえず以下の2案が思い浮かびました。

ジェネリックを使って汎用的に実装する

IXunitSerializable を実装した ValidateTestCase を作成し、ジェネリックでテストケースクラスの型を指定してやります。

[Theory,
    MemberData(nameof(TestData))]
public void TestMethod(ValidateTestCase<TestCaseData> testCaseData)
{
    var param1 = testCaseData.Param.Param1;
    var param2 = testCaseData.Param.Param2;
    
    // Do something test
}

public class ValidateTestCase<T> : IXunitSerializable
{
    public T Param { get; set; }

    public void Serialize(IXunitSerializationInfo info)
    {
        // JSON を使う場合
        info.AddValue(nameof(Param), JsonConvert.SerializeObject(Param));

        // ハッシュ値を使う場合
        // info.AddValue(nameof(Param), GetHashCode());

        // GUID を使う場合
        // info.AddValue(nameof(Param), Guid.NewGuid().ToString());
    }

    public void Deserialize(IXunitSerializationInfo info) { }
}

public class TestCaseData
{
    public int Param1 { get; set; }
    public string Param2 { get; set; }
}

public static IEnumerable<object[]> TestData()
{
    yield return new object[] {
        new ValidateTestCase<TestCaseData>{
            Param = new TestCaseData(){
                Param1 = 0,
                Param2 = "hoge"
            }
        }
    };
    yield return new object[] {
        new ValidateTestCase<TestCaseData>{
            Param = new TestCaseData(){
                Param1 = 1,
                Param2 = "fuga"
            }
        }
    };
}

Serialize メソッドで登録する文字列を ToString() から JSON でのシリアライズに変更しているのがポイントです。

クラスに対して ToString() するとクラス名が出力されるので、全てのテストで同じ文字列になってしまい、テストエクスプローラーでは単一のテストとして表示されてしまいました。
この文字列は一連のテストケースにおいてユニークでないとダメなようです。
なので、テストパラメーターには重複はないという前提で、JSON でシリアライズしてみました。

ただし、テストケースクラス TestCaseData にデリゲートを含んでいる場合は上手くいかなかったので、そういう場合はハッシュ値や GUID などを使ったほうが良いと思います。
(ハッシュ値は被ることもあるかもしれないので、GUID のほうが確実か?)

抽象クラスを作成して汎用的に実装する

上記だと毎回 ValidateTestCaseT の2つのクラスをインスタンス化しないといけなくてちょっと面倒なので、同じような感じで抽象クラスで実装してみました。

自クラス this に対してシリアライズを行っています。

[Theory,
    MemberData(nameof(TestData))]
public void TestMethod(TestCaseData testCaseData)
{
    var param1 = testCaseData.Param1;
    var param2 = testCaseData.Param2;
    
    // Do something test
}

public abstract class XunitSerializableBase : IXunitSerializable
{
    public void Serialize(IXunitSerializationInfo info)
    {
        // JSON を使う場合
        info.AddValue(nameof(XunitSerializableBase), JsonConvert.SerializeObject(Param));

        // ハッシュ値を使う場合
        // info.AddValue(nameof(XunitSerializableBase), GetHashCode());

        // GUID を使う場合
        // info.AddValue(nameof(XunitSerializableBase), Guid.NewGuid().ToString());
    }

    public void Deserialize(IXunitSerializationInfo info) { }
}

public class TestCaseData : XunitSerializableBase
{
    public int Param1 { get; set; }
    public string Param2 { get; set; }
}


public static IEnumerable<object[]> TestData()
{
    yield return new object[] {
        new TestCaseData{
            Param1 = 0,
            Param2 = "hoge"
        }
    };
    yield return new object[] {
        new TestCaseData{
            Param1 = 1,
            Param2 = "fuga"
        }
    };
}

ちょっとだけ記述量が減りました。

他の案は?

上記のリンク先にある DjvuTheory が便利そうだったので試しましたが、.NET Core 2.2 だと動きませんでした。
自分で修正しようともしてみましたが、何故かうまくいかず断念。
これが使えたら TheoryDjvuTheory に置換するだけでいけそうだったのに残念です。