複雑なパラメーターを[理論]に渡す


98

Xunitには優れた機能がありTheoryます。属性を使用して1つのテストを作成し、データを配置できます。InlineData属性に多くのテストを生成し、それらすべてをテストします。

私はこのような何かをしたいが、私のメソッドへのパラメータは、(のような「単純なデータ」ではありませんstringintdouble)が、私のクラスの一覧:

public static void WriteReportsToMemoryStream(
    IEnumerable<MyCustomClass> listReport,
    MemoryStream ms,
    StreamWriter writer) { ... }

3
それはあなたの環境に理にかなっている場合は、ノイズの少ないたくさんのF#でそれを行うことができます: - stackoverflow.com/a/35127997/11635
ルーベンBartelink

1
単体テストで複雑なオブジェクトをパラメーターとしてTestメソッドの複雑な型に
Iman Bahrampour

回答:


137

xxxxDataXUnitには多くの属性があります。たとえば、PropertyData属性を確認してください。

を返すプロパティを実装できますIEnumerable<object[]>object[]このメソッドが生成するそれぞれは、その後、[Theory]

別のオプションはですClassData。これは同じように機能しますが、異なるクラス/名前空間のテスト間で「ジェネレータ」を簡単に共有でき、「データジェネレータ」を実際のテストメソッドから分離します。

ここからこれらの例を参照してください

PropertyDataの例

public class StringTests2
{
    [Theory, PropertyData(nameof(SplitCountData))]
    public void SplitCount(string input, int expectedCount)
    {
        var actualCount = input.Split(' ').Count();
        Assert.Equal(expectedCount, actualCount);
    }

    public static IEnumerable<object[]> SplitCountData
    {
        get
        {
            // Or this could read from a file. :)
            return new[]
            {
                new object[] { "xUnit", 1 },
                new object[] { "is fun", 2 },
                new object[] { "to test with", 3 }
            };
        }
    }
}

ClassDataの例

public class StringTests3
{
    [Theory, ClassData(typeof(IndexOfData))]
    public void IndexOf(string input, char letter, int expected)
    {
        var actual = input.IndexOf(letter);
        Assert.Equal(expected, actual);
    }
}

public class IndexOfData : IEnumerable<object[]>
{
    private readonly List<object[]> _data = new List<object[]>
    {
        new object[] { "hello world", 'w', 6 },
        new object[] { "goodnight moon", 'w', -1 }
    };

    public IEnumerator<object[]> GetEnumerator()
    { return _data.GetEnumerator(); }

    IEnumerator IEnumerable.GetEnumerator()
    { return GetEnumerator(); }
}

@dcastro:ええ、私は実際に元のxunitドキュメントでいくつかを検索しています
ケツァルコアトル

2
@Nick:PropertyDataに似ていることに同意しますが、その理由も指摘しましたstatic。そういうわけで私はしません。ClassDataは、静態から脱出したいときです。そうすることで、ジェネレーターをより簡単に再利用(ネスト)できます。
ケツァルコアトル2014

1
ClassDataで何が起こったのですか?私はxUnit2.0でそれを見つけることができません、今のところ、クラスの新しいインスタンスを作成してそれを返す静的メソッドでMemberDataを使用しています。
Erti-Chris Eelmaa

14
@Erti、属性[MemberData("{static member}", MemberType = typeof(MyClass))]を置き換えるために使用しClassDataます。
Junle Li

6
C#6以降nameof、プロパティ名をハードコーディングするのではなく、キーワードを使用することをお勧めします(簡単に、しかし静かに中断します)。
sara

40

@Quetzalcoatlの回答を更新するには:属性 [PropertyData][MemberData]、を返す静的メソッド、フィールド、またはプロパティの文字列名を引数として取るように変更されましたIEnumerable<object[]>。(テストケースを一度に1つずつ実際に計算して、計算された結果を得ることができる反復子メソッドがあると、私は特に素晴らしいと思います。)

列挙子によって返されるシーケンスの各要素はでありobject[]、各配列は同じ長さである必要があり、その長さはテストケースへの引数の数でなければなりません(属性で注釈が付けられ、[MemberData]各要素は対応するメソッドパラメーターと同じ型である必要があります) 。(または多分それらは変換可能な型であるかもしれません、私は知りません。)

(参照xUnit.net 2014年3月のリリースノートサンプルコードと実際のパッチを。)


2
@davidbakコプレックスがなくなりました。リンクが機能していない
Kishan Vaishnav

11

匿名オブジェクト配列を作成することは、データを構築する最も簡単な方法ではないため、プロジェクトでこのパターンを使用しました

まず、再利用可能な共有クラスをいくつか定義します

//http://stackoverflow.com/questions/22093843
public interface ITheoryDatum
{
    object[] ToParameterArray();
}

public abstract class TheoryDatum : ITheoryDatum
{
    public abstract object[] ToParameterArray();

    public static ITheoryDatum Factory<TSystemUnderTest, TExpectedOutput>(TSystemUnderTest sut, TExpectedOutput expectedOutput, string description)
    {
        var datum= new TheoryDatum<TSystemUnderTest, TExpectedOutput>();
        datum.SystemUnderTest = sut;
        datum.Description = description;
        datum.ExpectedOutput = expectedOutput;
        return datum;
    }
}

public class TheoryDatum<TSystemUnderTest, TExecptedOutput> : TheoryDatum
{
    public TSystemUnderTest SystemUnderTest { get; set; }

    public string Description { get; set; }

    public TExpectedOutput ExpectedOutput { get; set; }

    public override object[] ToParameterArray()
    {
        var output = new object[3];
        output[0] = SystemUnderTest;
        output[1] = ExpectedOutput;
        output[2] = Description;
        return output;
    }

}

これで、個々のテストとメンバーのデータが書きやすくなり、よりクリーンになります...

public class IngredientTests : TestBase
{
    [Theory]
    [MemberData(nameof(IsValidData))]
    public void IsValid(Ingredient ingredient, bool expectedResult, string testDescription)
    {
        Assert.True(ingredient.IsValid == expectedResult, testDescription);
    }

    public static IEnumerable<object[]> IsValidData
    {
        get
        {
            var food = new Food();
            var quantity = new Quantity();
            var data= new List<ITheoryDatum>();

            data.Add(TheoryDatum.Factory(new Ingredient { Food = food }                       , false, "Quantity missing"));
            data.Add(TheoryDatum.Factory(new Ingredient { Quantity = quantity }               , false, "Food missing"));
            data.Add(TheoryDatum.Factory(new Ingredient { Quantity = quantity, Food = food }  , true,  "Valid"));

            return data.ConvertAll(d => d.ToParameterArray());
        }
    }
}

文字列Descriptionプロパティは、多くのテストケースの1つが失敗したときに骨を投げます


1
私はこれが好き; 90以上のプロパティの検証を検証する必要がある非常に複雑なオブジェクトには、実際の可能性があります。単純なJSONオブジェクトを渡し、それを逆シリアル化して、テストの反復のためにデータを生成できます。よくやった。
Gustyn '20

1
IsValid Testmethodのパラメーターは混同されていませんか?IsValid(ingrediant、exprectedResult、testDescription)であるべきではありませんか?
パスタクール

9

Manufacturerクラスを持つ複雑なCarクラスがあるとします。

public class Car
{
     public int Id { get; set; }
     public long Price { get; set; }
     public Manufacturer Manufacturer { get; set; }
}
public class Manufacturer
{
    public string Name { get; set; }
    public string Country { get; set; }
}

Carクラスに入力して、Theoryテストに渡します。

したがって、以下のようにCarクラスのインスタンスを返す 'CarClassData'クラスを作成します。

public class CarClassData : IEnumerable<object[]>
    {
        public IEnumerator<object[]> GetEnumerator()
        {
            yield return new object[] {
                new Car
                {
                  Id=1,
                  Price=36000000,
                  Manufacturer = new Manufacturer
                  {
                    Country="country",
                    Name="name"
                  }
                }
            };
        }
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

テストメソッド(CarTest)を作成して、車をパラメータとして定義します。

[Theory]
[ClassData(typeof(CarClassData))]
public void CarTest(Car car)
{
     var output = car;
     var result = _myRepository.BuyCar(car);
}

理論上の複合型

幸運を


3
この回答は、選択した回答から欠落しているように見える理論入力としてカスタムタイプを渡す問題に明確に対処しています。
JDケイン

1
これはまさに私が探していたユースケースであり、複雑な型をパラメーターとして理論に渡す方法です。完璧に動作します!これは、MVPパターンのテストに実際に役立ちます。これで、ビューのさまざまなインスタンスをさまざまな状態でセットアップし、それらすべてを同じ理論に渡して、Presenterメソッドがそのビューに及ぼす影響をテストできます。大好きです!
Denis M. Kitchen、

3

あなたはこの方法を試すことができます:

public class TestClass {

    bool isSaturday(DateTime dt)
    {
       string day = dt.DayOfWeek.ToString();
       return (day == "Saturday");
    }

    [Theory]
    [MemberData("IsSaturdayIndex", MemberType = typeof(TestCase))]
    public void test(int i)
    {
       // parse test case
       var input = TestCase.IsSaturdayTestCase[i];
       DateTime dt = (DateTime)input[0];
       bool expected = (bool)input[1];

       // test
       bool result = isSaturday(dt);
       result.Should().Be(expected);
    }   
}

テストデータを保持する別のクラスを作成します。

public class TestCase
{
   public static readonly List<object[]> IsSaturdayTestCase = new List<object[]>
   {
      new object[]{new DateTime(2016,1,23),true},
      new object[]{new DateTime(2016,1,24),false}
   };

   public static IEnumerable<object[]> IsSaturdayIndex
   {
      get
      {
         List<object[]> tmp = new List<object[]>();
            for (int i = 0; i < IsSaturdayTestCase.Count; i++)
                tmp.Add(new object[] { i });
         return tmp;
      }
   }
}

1

私のニーズでは、いくつかのテストを通じて一連の「テストユーザー」を実行したかっただけですが、[ClassData]などは、必要なものに対してはやり過ぎに見えました(アイテムのリストが各テストにローカライズされていたため)。

だから私はテストの中に配列を入れて次のようにしました-外部からインデックスを付けました:

[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public async Task Account_ExistingUser_CorrectPassword(int userIndex)
{
    // DIFFERENT INPUT DATA (static fake users on class)
    var user = new[]
    {
        EXISTING_USER_NO_MAPPING,
        EXISTING_USER_MAPPING_TO_DIFFERENT_EXISTING_USER,
        EXISTING_USER_MAPPING_TO_SAME_USER,
        NEW_USER

    } [userIndex];

    var response = await Analyze(new CreateOrLoginMsgIn
    {
        Username = user.Username,
        Password = user.Password
    });

    // expected result (using ExpectedObjects)
    new CreateOrLoginResult
    {
        AccessGrantedTo = user.Username

    }.ToExpectedObject().ShouldEqual(response);
}

テストの意図を明確に保ちながら、これは私の目標を達成しました。インデックスを同期させる必要があるだけですが、それだけです。

結果は見栄えが良く、折りたたみ可能で、エラーが発生した場合は特定のインスタンスを再実行できます。

ここに画像の説明を入力してください


「結果は見栄えがよく、折りたたみ可能で、エラーが発生した場合は特定のインスタンスを再実行できます。」非常に良い点です。の主な欠点はMemberData、特定のテスト入力でテストを表示または実行できないことです。吸います。
Oliver Pearmain

実際、私はMemberDataあなたがTheoryDataとオプションでを使用すると可能であることを理解しましたIXunitSerializable。詳細と例はこちら... github.com/xunit/xunit/issues/429#issuecomment-108187109
Oliver Pearmain

1

これが私があなたの問題を解決した方法です、私は同じシナリオを持っていました。したがって、カスタムオブジェクトと、実行ごとに異なる数のオブジェクトをインライン化します。

    [Theory]
    [ClassData(typeof(DeviceTelemetryTestData))]
    public async Task ProcessDeviceTelemetries_TypicalDeserialization_NoErrorAsync(params DeviceTelemetry[] expected)
    {
        // Arrange
        var timeStamp = DateTimeOffset.UtcNow;

        mockInflux.Setup(x => x.ExportTelemetryToDb(It.IsAny<List<DeviceTelemetry>>())).ReturnsAsync("Success");

        // Act
        var actual = await MessageProcessingTelemetry.ProcessTelemetry(JsonConvert.SerializeObject(expected), mockInflux.Object);

        // Assert
        mockInflux.Verify(x => x.ExportTelemetryToDb(It.IsAny<List<DeviceTelemetry>>()), Times.Once);
        Assert.Equal("Success", actual);
    }

これが私の単体テストです。paramsパラメータに注目してください。これにより、異なる数のオブジェクトを送信できます。そして今私のDeviceTelemetryTestDataクラス:

    public class DeviceTelemetryTestData : IEnumerable<object[]>
    {
        public IEnumerator<object[]> GetEnumerator()
        {
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
            yield return new object[] { new DeviceTelemetry { DeviceId = "asd" }, new DeviceTelemetry { DeviceId = "qwe" } };
        }

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

それが役に立てば幸い !


-1

ここで間違えたと思います。xUnit Theory属性の実際の意味:このテスト対象の関数が受け取るパラメーターとして特殊な値やランダムな値を送信して、この関数をテストしたいとします。など、あなたは次の属性として定義するものということを意味は、: 、InlineDataPropertyDataClassDataなど。は、これらのパラメータのソースになります。つまり、これらのパラメーターを提供するには、ソースオブジェクトを作成する必要があります。あなたの場合ClassData、ソースとしてオブジェクトを使用する必要があると思います。また、-をClassData継承することに注意してくださいIEnumerable<>-これは、生成されるパラメーターの別のセットが、テスト中の関数の受信パラメーターとして使用されるたびにIEnumerable<>、値が生成されます。

ここに例: Tom DuPont .NET

例は正しくない可能性があります-xUnitを長期間使用していません

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.