Pass complex parameters to [Theory]

Xunit has a nice feature: you can create one test with a Theory attribute and put data in InlineData attributes, and xUnit will generate many tests, and test them all.

I want to have something like this, but the parameters to my method are not 'simple data' (like string, int, double), but a list of my class:

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

XUnit 中有许多 xxxxData属性。

可以实现返回 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 }
};
}

免责声明:)

最后一次检查是在 c # 5.0和 xunit 2.4.1上, dotnetfiddle.net 是@20210903。.失败了。我不能让一个试跑选手掺和进来。但至少它编译得很好。请注意,这本书最初是在几年前写的,情况发生了一些变化。我根据我的直觉和意见修改了它们。那么。.它可能包含不明显的输入错误,否则明显的错误,会立即出现在运行时,和痕迹的牛奶和坚果。

我想你搞错了。XUnit Theory属性的实际含义: 您希望通过将特殊/随机值作为参数发送给正在测试的函数来测试这个函数。这意味着您定义为下一个属性的内容,例如: InlineDataPropertyDataClassData等等。将是这些参数的源。这意味着您应该构造源对象来提供这些参数。在您的例子中,我想您应该使用 ClassData对象作为源。另外-请注意,ClassData继承自: IEnumerable<>-这意味着每次另一组生成的参数将被用作传入参数的功能-在测试中,直到 IEnumerable<>产生的值。

例如: 汤姆杜邦.NET

例子可能是不正确的-我没有使用 xUnit 很长一段时间

更新@Quetzalcoatl 的回答: 属性 [PropertyData]已经被 [MemberData]取代,[MemberData]以返回 IEnumerable<object[]>的任何静态方法、字段或属性的字符串名称作为参数。(我发现有一个迭代器方法特别好,它实际上可以一次测试一个 计算测试用例,并在计算时生成它们。)

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

(见 XUnit.net 2014年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;
}
}
}

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

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

//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, 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属性是当您的许多测试用例中的一个失败时抛给您一根骨头。

出于我的需要,我只是想通过一些测试来运行一系列的“测试用户”——但是[ ClassData ]等等似乎对我所需要的东西过分了(因为每个测试的项目列表都是本地化的)。

所以我做了以下操作,在 test 内部使用一个数组——从外部建立索引:

[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

假设我们有一个复杂的 Car 类,它有一个 Manufacturers 类:

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

我们要通过汽车课的理论测试。

因此,创建一个“ 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);
}

complex type in theory

如果你想把汽车对象的列表传递给 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;
}

祝你好运

这就是我怎么解决你的问题,我有同样的情况。因此在每次运行时与自定义对象和不同数量的对象内联。

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

希望能有帮助!

xUnit.Sdk为您提供了 DataAttribute类,您可以继承和重写它的 GetData方法,并使用它来传递您想要传递的任何内容。.

我通常在 DataTestBuilders 模式旁边使用它,并构建类似的东西。

public class ValidComplexObjectDataSource : DataAttribute
{
public override IEnumerable<object[]> GetData(MethodInfo testMethod)
{
yield return new object[] {
ComplexObjectBuilder
.BasicComplexObject()
.Build()
};


yield return new object[] {
ComplexObjectBuilder
.BasicComplexObject()
.WithoutSomeAttribute()
.Build()
};


// ... list all test cases you want to pass to your method
}
}

这个 ComplexObjectBuilder可以是任何您的对象,强烈建议检查构建器模式

[Theory]
[Trait("Validation", "CreateXYZCommand")]
[ValidComplexObjectDataSource]
public void CreateXYZCommandValidator_WithValidInput_ShouldPassAllValidations(CreateComplexObjectInput createComplexObjectInput)
{
var command = new CreateXYZCommand(createComplexObjectInput);
var result = _validator.TestValidate(command);
result.ShouldNotHaveAnyValidationErrors();
}

我只演示了一个对象,你可以生成一个对象数组。

yield return new object[] {
ComplexObject_1,
ComplexObject_2,
string_attribute,
int_attribute
};

并将这些作为测试用例的参数。

可以利用 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{ ... },
...
};
}
}