传递复杂参数到[Theory]函数

154
Xunit有一个不错的功能:你可以创建一个带有Theory属性并在其中使用InlineData属性放置数据的测试方法,xUnit会生成许多测试,并对它们进行测试。
我想要像这样的东西,但是我方法的参数不是“简单数据”(如stringintdouble),而是我的类的列表。
public static void WriteReportsToMemoryStream(
    IEnumerable<MyCustomClass> listReport,
    MemoryStream ms,
    StreamWriter writer) { ... }

2
一个完整的指南,将复杂对象作为参数发送到测试方法中[单元测试中的复杂类型] (https://dev59.com/QWEh5IYBdhLWcg3wvFfY#56413307)。 - Iman Bahrampour
1
被接受的答案传递基本数据类型而不是复杂类型到该理论!! 第三个答案正是答案。在xunit中传递复杂参数 - 2nyacomputer
13个回答

192

XUnit中有许多xxxxData属性。例如,可以查看MemberData属性。

您可以实现一个返回IEnumerable<object[]>的属性。此方法生成的每个object[]将被“解包”为单个调用您的[Theory]方法的参数。

请参见这里的示例

以下是一些示例,仅供快速查看。

MemberData示例:就在这里手边

public class StringTests2
{
    [Theory, MemberData(nameof(SplitCountData))]
    public void SplitCount(string input, int expectedCount)
    {
        var actualCount = input.Split(' ').Count();
        Assert.Equal(expectedCount, actualCount);
    }
 
    public static IEnumerable<object[]> SplitCountData => 
        new List<object[]>
        {
            new object[] { "xUnit", 1 },
            new object[] { "is fun", 2 },
            new object[] { "to test with", 3 }
        };
}

XUnit < 2.0:另一种选项是 ClassData,它的工作方式相同,但允许在不同的类/命名空间中轻松共享“生成器”,同时将“数据生成器”与实际测试方法分离。

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(); }
}

XUnit >= 2.0:现在不再使用ClassData,而是有一个[MemberData]的“重载”,可以使用其他类的静态成员。下面的示例已更新以使用它,因为XUnit<2.x现在相当古老。 另一个选项是ClassData,它的工作方式相同,但允许在不同的类/命名空间中轻松共享“生成器”,并将“数据生成器”与实际测试方法分离。

MemberData 示例:请查看其他类型

public class StringTests3
{
    [Theory, MemberData(nameof(IndexOfData.SplitCountData), MemberType = 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[]>
{
    public static IEnumerable<object[]> SplitCountData => 
        new List<object[]>
        {
            new object[] { "hello world", 'w', 6 },
            new object[] { "goodnight moon", 'w', -1 }
        };
}

免责声明 :)

最后检查于2021年9月3日,使用C# 5.0和xunit 2.4.1在dotnetfiddle.net上进行测试时失败了。我无法将测试运行器混合到那个代码片段中。但至少它编译得很好。请注意,这是几年前编写的,事情有所变化。我根据我的直觉和评论进行了修复。因此..它可能包含难以发现的拼写错误、明显的bug,在运行时会立即出现,并且有牛奶和坚果的痕迹。


2
@Nick:我同意这与PropertyData相似,但你也指出了原因:“static”。这正是我不会这样做的原因。当你想要摆脱静态时,就可以使用ClassData。通过这样做,你可以更容易地重用(即嵌套)生成器。 - quetzalcoatl
1
@quetzalcoatl 噢,我明白了。您可能有多个数据源具有一些共同点,因此您只需在基类中编写一次即可,其他数据源可以继承它。 - Nick
1
你有任何关于ClassData的想法吗?我在xUnit2.0中找不到它,目前我正在使用一个静态方法的MemberData,并创建类的新实例并返回。 - Erti-Chris Eelmaa
16
@Erti,请使用[MemberData("{static member}", MemberType = typeof(MyClass))]替换ClassData属性。 - Junle Li
8
从C#6开始,建议使用nameof关键字而不是硬编码属性名称(易于静默地出现错误)。 - sara
显示剩余5条评论

56

假设我们有一个复杂的Car类,其中包含一个Manufacturer类:

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类到一个理论测试中。

因此,请创建一个名为'CarClassData'的类,并返回以下代码所示的Car类实例:

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);
}

理论中的复杂类型

如果您要将汽车对象列表传递给 Theory,则将 CarClassData 更改如下:

public class CarClassData : IEnumerable<object[]>
    {
        public IEnumerator<object[]> GetEnumerator()
        {
            yield return new object[] {
                new List<Car>()
                {
                new Car
                {
                  Id=1,
                  Price=36000000,
                  Manufacturer = new Manufacturer
                  {
                    Country="Iran",
                    Name="arya"
                  }
                },
                new Car
                {
                  Id=2,
                  Price=45000,
                  Manufacturer = new Manufacturer
                  {
                    Country="Torbat",
                    Name="kurosh"
                  }
                }
                }
            };
        }
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

那么这个理论就是:

[Theory]
[ClassData(typeof(CarClassData))]
public void CarTest(List<Car> cars)
{
   var output = cars;
}

祝你好运


11
此回答明确解答了如何将自定义类型作为Theory输入的问题,而这似乎在所选答案中被忽略了。 - J.D. Cain
1
这正是我正在寻找的用例,即如何将复杂类型作为参数传递给Theory。完美运行!这对于测试MVP模式真的很有帮助。现在我可以设置许多不同状态的View实例,并将它们全部传递到相同的Theory中,以测试Presenter方法对该视图产生的影响。太喜欢了! - Denis M. Kitchen
2
如何在汽车类数据中返回多个对象? - Ash A
1
添加多个yield return语句,使用各种场景,您可以执行尽可能多的测试。 - srbrills
@AshA,抱歉我晚看到了你的评论。帖子已经编辑过了。 - Iman Bahrampour

49

更新@Quetzalcoatl的回答:属性[PropertyData]已被[MemberData]取代,后者的参数是任何返回IEnumerable<object[]>的静态方法、字段或属性的字符串名称。 (我发现拥有一个可以逐个计算测试用例并在计算时产生的迭代器方法非常好。)

枚举器返回序列中的每个元素都是object[],每个数组必须具有相同的长度,并且该长度必须是您的测试用例(使用属性[MemberData]进行注释)的参数数量。每个元素都必须具有与相应方法参数相同的类型。(或者它们可以是可转换的类型,我不知道。)

(请参阅xUnit.net 2014年3月发布说明实际修补程序及示例代码。)


5
@davidbak Codplex已经不存在了,链接无法使用。 - Kishan Vaishnav
@KishanVaishnav 我认为没什么变化,我只是将属性从PropertyData改为了MemberData。 - Andes Lam
1
@KishanVaishnav 更新了 Patch 的链接 :-) - Paul Farry

11

创建匿名对象数组并不是构建数据的最简单方式,因此我在项目中使用了这种模式。

首先定义一些可重用的共享类:

//https://dev59.com/QWEh5IYBdhLWcg3wvFfY
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, TExpectedOutput> : 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
我喜欢这个;它有一些真正的潜力,可以用来验证我需要验证90多个属性的非常复杂的对象。我可以传入一个简单的JSON对象,反序列化它,并为测试迭代生成数据。干得好。 - Gustyn
1
参数是否混淆了?IsValid测试方法的参数不应该是IsValid(ingrediant, exprectedResult, testDescription)吗? - macf00bar

6

针对我的需求,我只需要通过一系列测试来运行“测试用户”,但是[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);
}

这样做实现了我的目标,同时保持测试意图的清晰。你只需要确保索引同步即可。
结果看起来很好,它是可折叠的,如果出现错误,你可以重新运行特定的实例: enter image description here

“结果看起来很好,它是可折叠的,如果出现错误,你可以重新运行特定的实例。”非常好的观点。 MemberData 的一个主要缺点似乎是您无法查看或使用特定的测试输入运行测试。这很糟糕。 - Oliver Pearmain
其实,我刚刚发现如果你使用TheoryData和可选的IXunitSerializable,就可以通过MemberData实现。更多信息和示例请参见此处... https://github.com/xunit/xunit/issues/429#issuecomment-108187109 - Oliver Pearmain

4
您可以尝试这种方法:
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
这是我解决您的问题的方法,我遇到了同样的情况。因此,根据自定义对象和每次运行时不同数量的对象进行内联处理。
    [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
这是我对问题的解决方案。

https://github.com/xunit/xunit/issues/2760

优点是它将yield和枚举器从用户代码中隐藏。

引入一个新的属性和接口。

public class InlineObjectDataAttribute<T> : ClassDataAttribute where T : IInlineObjects
{
    public InlineObjectDataAttribute() : base(typeof(GenericTestData)) { }

    class GenericTestData : IEnumerable<object?[]>
    {
        public IEnumerator<object?[]> GetEnumerator()
        {
            foreach (var item in T.GetObjects())
            {
                yield return item;
            }
        }

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

public interface IInlineObjects
{
    static abstract IEnumerable<object?[]> GetObjects();
    static object?[] Line(params object?[] data) => data;
}

在你的测试类中使用以下内容:
using static IInlineObjects;

class MyTestClass : IInlineObjects
{
    public static IEnumerable<object?[]> GetObjects() => new object?[][]
    {
        Line(180d, new DateTime(2000, 1, 1, 6, 0, 0)),
    };
}

[Theory]
[InlineObjectData<MyTestClass>]
public void TestMethod(object o1, object o2)

1
尽管这个问题已经有了答案,我还想在这里提出一些改进。
InlineData 属性中只能传递值类型是 C# 特性的限制,而不是 xUnit 本身的限制。
请参考以下编译器错误:Compiler Error CS0182

0

您可以使用TheoryData来处理类等复杂类型。

[Theory, MemberData(nameof(CustomClassTests))]
public async Task myTestName(MyCustomClass customClassTestData) { ... }

public record MyCustomClass { ... }

public static TheoryData<MyCustomClass> CustomClassTests {
    get {
        return new() {
            new MyCustomClass{ ... },
            new MyCustomClass{ ... },
            ...
        }; 
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接