泛型类型的单元测试
Unit Test with Generics types
我想用泛型测试单元测试,我正在努力寻找正确的方法。
我有这个
[TestCase(typeof(CalendarGeneralCsv), typeof(CalendarGeneralCsvMap), 121)]
public void ReadFromCsvFileWithConfigurationMapTest<T,Tmap>(T t, Tmap tmap, int totalRowsExptected)
{
//Arrange
//Act
var records = csvService.ReadFileCsv<T, Tmap>(_csvToRead, ",") as IEnumerable<object>;
var result = new List<object>(records);
//Assert
result.Should().NotBeNullOrEmpty();
result.Should().HaveCount(totalRowsExptected);
}
错误在这一行
var records = csvService.ReadFileCsv<T, Tmap>(_csvToRead, ",") as IEnumerable<object>;
说T,Tmap必须是引用类型
考虑使用反射调用被测通用对象
[TestCase(typeof(CalendarGeneralCsv), typeof(CalendarGeneralCsvMap), 121)]
public void ReadFromCsvFileWithConfigurationMapTest(Type t, Type tmap, int totalRowsExptected) {
//Arrange
//...
var serviceType = csvService.GetType();
var method = serviceType.GetMethod("ReadFileCsv");
var genericMethod = method.MakeGenericMethod(t, tmap);
var parameters = new object[] { _csvToRead, "," };
//Act
var records = genericMethod.Invoke(csvService, parameters) as IEnumerable<object>;
//Above same as csvService.ReadFileCsv<T, Tmap>(_csvToRead, ",") as IEnumerable<object>;
//Assert
records.Should().NotBeNull();
var result = new List<object>(records);
result.Should().NotBeNullOrEmpty();
result.Should().HaveCount(totalRowsExptected);
}
使用 csvService
,通过 GetType()
获取类型
var serviceType = csvService.GetType();
为了访问其会员信息。
按名称查找要调用的所需成员
var method = serviceType.GetMethod("ReadFileCsv");
并为泛型参数使用提供的类型参数
var genericMethod = method.MakeGenericMethod(t, tmap);
可以使用传递的参数在服务实例上调用通用成员。
var records = genericMethod.Invoke(csvService, new object[] { _csvToRead, "," }) as IEnumerable<object>;
或者,如果您不打算在不同类型的测试中使用多个 TestCase
属性,则无需为测试提供任何通用参数。您可以直接将类型显式传递给类型参数:
public void ReadFromCsvFileWithConfigurationMapTest()
{
//Arrange
//Act
var records = csvService.ReadFileCsv<CalendarGeneralCsv, CalendarGeneralCsvMap>(_csvToRead, ",") as IEnumerable<object>;
var result = new List<object>(records);
//Assert
result.Should().NotBeNullOrEmpty();
result.Should().HaveCount(121);
}
如果您确实想要 运行 多个 types/values 的相同测试用例,您可以将实际测试逻辑提取到通用方法中。然后,您可以为要测试的每组数据创建一个新测试,将类型显式传递到泛型方法中:
[Test]
public void ReadFromCsvFileWithConfigurationMapTest() => ReadFromCsvFile<CalendarGeneralCsv, CalendarGeneralCsvMap>(121);
[Test]
public void ReadFromCsvFileWithOtherMapTest() => ReadFromCsvFile<CalendarGeneralCsv, OtherGeneralCsvMap>(151);
private void ReadFromCsvFile<T, TMap>(int expectedValue)
{
//Arrange
//Act
var records = csvService.ReadFileCsv<T, TMap>(_csvToRead, ",") as IEnumerable<object>;
var result = new List<object>(records);
//Assert
result.Should().NotBeNullOrEmpty();
result.Should().HaveCount(expectedValue);
}
虽然已经有一个被接受的答案(注意:自从我最初发布以来它已经改变),我想提供一种使用反射的替代方法。将测试方法拆分为两种方法,跳板方法和通用测试方法。有几个优点:
通用测试方法看起来更像任何其他测试方法。它没有混入不相关的反射。
通用测试方法可以正常单步执行,也是因为没有混入不相关的反射
对被测组件的更改更有可能在测试项目中触发编译器错误,因此您知道需要更新通用测试方法,可能还有跳板方法。另外,由于抛出异常的位置,在 运行 时更清楚这是因为支持反射而不是组件的使用方式。
跳板方法不需要知道被测组件的任何信息,只需要知道如何调用通用测试方法。
可以轻松且一致地复制该模式,因为几乎没有变化。
这是一个基于问题和已接受答案的示例:
[TestCase(typeof(CalendarGeneralCsv), typeof(CalendarGeneralCsvMap), 121)]
[TestCase(typeof(CalendarCustomCsv), typeof(CalendarCustomCsvMap), 80)]
public void ReadFromCsvFileWithConfigurationMapTest(Type t, Type tmap, int totalRowsExpected)
{
GetType().GetMethod(nameof(GenericReadFromCsvFileWithConfigurationMapTest))
.MakeGenericMethod(t, tmap) // <-- Type parameters go here
.Invoke(this, new object[] { totalRowsExpected }); // <-- inputs go here
}
public void GenericReadFromCsvFileWithConfigurationMapTest<T, Tmap>(int totalRowsExpected)
where T : class
where Tmap : class
{
// Arrange
// Act
var records = csvService.ReadFileCsv<T, Tmap>(_csvToRead, ",") as IEnumerable<object>;
// Assert
records.Should().NotBeNull();
var result = new List<object>(records);
result.Should().NotBeNullOrEmpty();
result.Should().HaveCount(totalRowsExptected);
}
兴趣点
它使用 GetType()
因为它正在寻找相同类型的方法(测试 class)。这样可以减少变化,从而可以更轻松地复制图案。
通用测试方法有不同的名称(只要不同就无所谓),因此 GetMethod
调用不需要指定参数类型。该名称应该只有一种方法,它是 public,因此它也不需要 BindingFlags
。或者,您可以将其设为私有,只需添加 BindingFlags.NonPublic | BindingFlags.Instance
。 注意:并非所有框架版本都有重载 BindingFlags
。如果您想将其设为私有,则必须找到替代方案。
通用测试方法需要包含约束。这使得约束成为测试的正式部分。如果不满足约束,反射将在 运行 时以任何一种方式失败,但是将它们放在通用测试方法中,您可能会从一开始就编写更好的测试。您提到 T
和 Tmap
必须是引用类型,因此它们包含在上面。
最后,您的跳板能够定义多个测试用例,正如您所指出的,您需要能够做到,所以我在上面包含了另一个日历和映射。
我通常不会回复已经有几个答案并且一个被接受的地方,但它们似乎都基于测试方法不能通用的假设。他们绝对可以。我的记忆告诉我,这曾经有据可查,但似乎不再存在了——或者我的记忆有误——这解释了为什么你可能认为这不可能。
这里的通用解决方案可能不是最好的,但尝试这似乎是一件有趣的事情,并且可能会更好或阐明为什么公认的解决方案更好。我只能根据已经提供的信息走这么远,但如果 jolynice 愿意合作,也许我们可以学到一些东西。 :-)
所以...这是解决方案的初始镜头,如果返回更多信息,我将对其进行编辑。
题中原解出错是因为不满足泛型方法ReadFileCsv<T, Tmap>(...)
中的约束。我们不知道它们是什么,但从错误来看它们包括 T : class
和 Tmap : class
。因此,正确答案的第一步是在测试方法本身上重现被调用方法的所有约束。
更新:此代码实际上不起作用。简而言之,我在本地有这个功能,我认为它已经被添加到 NUnit 但它没有。另请参阅下面的更新文本...
[TestCase(typeof(CalendarGeneralCsv), typeof(CalendarGeneralCsvMap), 121)]
public void ReadFromCsvFileWithConfigurationMapTest<T,Tmap>(int totalRowsExptected)
where T : class
where Tmap : class
{
//Arrange
//Act
var records = csvService.ReadFileCsv<T, Tmap>(_csvToRead, ",") as IEnumerable<object>;
var result = new List<object>(records);
//Assert
result.Should().NotBeNullOrEmpty();
result.Should().HaveCount(totalRowsExptected);
}
更新以回应@Jannes 的评论
您可以在 C# 中创建不带参数的泛型方法。如果您使用这样的方法作为测试方法,NUnit 将需要知道用于调用它的实际类型。很遗憾,没有这样的方法。
目前,NUnit 只能从您提供的参数中推断出实际类型。这意味着泛型方法的每个参数类型必须至少有一个参数。
这显然是 NUnit 中的一个漏洞,并且在 GitHub 的各种问题中都对此进行了讨论。到目前为止,还没有任何提案被接受。例如,请参阅 https://github.com/nunit/nunit/issues 上的问题 150、1215、2562 和 3576。
我想用泛型测试单元测试,我正在努力寻找正确的方法。
我有这个
[TestCase(typeof(CalendarGeneralCsv), typeof(CalendarGeneralCsvMap), 121)]
public void ReadFromCsvFileWithConfigurationMapTest<T,Tmap>(T t, Tmap tmap, int totalRowsExptected)
{
//Arrange
//Act
var records = csvService.ReadFileCsv<T, Tmap>(_csvToRead, ",") as IEnumerable<object>;
var result = new List<object>(records);
//Assert
result.Should().NotBeNullOrEmpty();
result.Should().HaveCount(totalRowsExptected);
}
错误在这一行
var records = csvService.ReadFileCsv<T, Tmap>(_csvToRead, ",") as IEnumerable<object>;
说T,Tmap必须是引用类型
考虑使用反射调用被测通用对象
[TestCase(typeof(CalendarGeneralCsv), typeof(CalendarGeneralCsvMap), 121)]
public void ReadFromCsvFileWithConfigurationMapTest(Type t, Type tmap, int totalRowsExptected) {
//Arrange
//...
var serviceType = csvService.GetType();
var method = serviceType.GetMethod("ReadFileCsv");
var genericMethod = method.MakeGenericMethod(t, tmap);
var parameters = new object[] { _csvToRead, "," };
//Act
var records = genericMethod.Invoke(csvService, parameters) as IEnumerable<object>;
//Above same as csvService.ReadFileCsv<T, Tmap>(_csvToRead, ",") as IEnumerable<object>;
//Assert
records.Should().NotBeNull();
var result = new List<object>(records);
result.Should().NotBeNullOrEmpty();
result.Should().HaveCount(totalRowsExptected);
}
使用 csvService
,通过 GetType()
var serviceType = csvService.GetType();
为了访问其会员信息。
按名称查找要调用的所需成员
var method = serviceType.GetMethod("ReadFileCsv");
并为泛型参数使用提供的类型参数
var genericMethod = method.MakeGenericMethod(t, tmap);
可以使用传递的参数在服务实例上调用通用成员。
var records = genericMethod.Invoke(csvService, new object[] { _csvToRead, "," }) as IEnumerable<object>;
或者,如果您不打算在不同类型的测试中使用多个 TestCase
属性,则无需为测试提供任何通用参数。您可以直接将类型显式传递给类型参数:
public void ReadFromCsvFileWithConfigurationMapTest()
{
//Arrange
//Act
var records = csvService.ReadFileCsv<CalendarGeneralCsv, CalendarGeneralCsvMap>(_csvToRead, ",") as IEnumerable<object>;
var result = new List<object>(records);
//Assert
result.Should().NotBeNullOrEmpty();
result.Should().HaveCount(121);
}
如果您确实想要 运行 多个 types/values 的相同测试用例,您可以将实际测试逻辑提取到通用方法中。然后,您可以为要测试的每组数据创建一个新测试,将类型显式传递到泛型方法中:
[Test]
public void ReadFromCsvFileWithConfigurationMapTest() => ReadFromCsvFile<CalendarGeneralCsv, CalendarGeneralCsvMap>(121);
[Test]
public void ReadFromCsvFileWithOtherMapTest() => ReadFromCsvFile<CalendarGeneralCsv, OtherGeneralCsvMap>(151);
private void ReadFromCsvFile<T, TMap>(int expectedValue)
{
//Arrange
//Act
var records = csvService.ReadFileCsv<T, TMap>(_csvToRead, ",") as IEnumerable<object>;
var result = new List<object>(records);
//Assert
result.Should().NotBeNullOrEmpty();
result.Should().HaveCount(expectedValue);
}
虽然已经有一个被接受的答案(注意:自从我最初发布以来它已经改变),我想提供一种使用反射的替代方法。将测试方法拆分为两种方法,跳板方法和通用测试方法。有几个优点:
通用测试方法看起来更像任何其他测试方法。它没有混入不相关的反射。
通用测试方法可以正常单步执行,也是因为没有混入不相关的反射
对被测组件的更改更有可能在测试项目中触发编译器错误,因此您知道需要更新通用测试方法,可能还有跳板方法。另外,由于抛出异常的位置,在 运行 时更清楚这是因为支持反射而不是组件的使用方式。
跳板方法不需要知道被测组件的任何信息,只需要知道如何调用通用测试方法。
可以轻松且一致地复制该模式,因为几乎没有变化。
这是一个基于问题和已接受答案的示例:
[TestCase(typeof(CalendarGeneralCsv), typeof(CalendarGeneralCsvMap), 121)]
[TestCase(typeof(CalendarCustomCsv), typeof(CalendarCustomCsvMap), 80)]
public void ReadFromCsvFileWithConfigurationMapTest(Type t, Type tmap, int totalRowsExpected)
{
GetType().GetMethod(nameof(GenericReadFromCsvFileWithConfigurationMapTest))
.MakeGenericMethod(t, tmap) // <-- Type parameters go here
.Invoke(this, new object[] { totalRowsExpected }); // <-- inputs go here
}
public void GenericReadFromCsvFileWithConfigurationMapTest<T, Tmap>(int totalRowsExpected)
where T : class
where Tmap : class
{
// Arrange
// Act
var records = csvService.ReadFileCsv<T, Tmap>(_csvToRead, ",") as IEnumerable<object>;
// Assert
records.Should().NotBeNull();
var result = new List<object>(records);
result.Should().NotBeNullOrEmpty();
result.Should().HaveCount(totalRowsExptected);
}
兴趣点
它使用 GetType()
因为它正在寻找相同类型的方法(测试 class)。这样可以减少变化,从而可以更轻松地复制图案。
通用测试方法有不同的名称(只要不同就无所谓),因此 GetMethod
调用不需要指定参数类型。该名称应该只有一种方法,它是 public,因此它也不需要 BindingFlags
。或者,您可以将其设为私有,只需添加 BindingFlags.NonPublic | BindingFlags.Instance
。 注意:并非所有框架版本都有重载 BindingFlags
。如果您想将其设为私有,则必须找到替代方案。
通用测试方法需要包含约束。这使得约束成为测试的正式部分。如果不满足约束,反射将在 运行 时以任何一种方式失败,但是将它们放在通用测试方法中,您可能会从一开始就编写更好的测试。您提到 T
和 Tmap
必须是引用类型,因此它们包含在上面。
最后,您的跳板能够定义多个测试用例,正如您所指出的,您需要能够做到,所以我在上面包含了另一个日历和映射。
我通常不会回复已经有几个答案并且一个被接受的地方,但它们似乎都基于测试方法不能通用的假设。他们绝对可以。我的记忆告诉我,这曾经有据可查,但似乎不再存在了——或者我的记忆有误——这解释了为什么你可能认为这不可能。
这里的通用解决方案可能不是最好的,但尝试这似乎是一件有趣的事情,并且可能会更好或阐明为什么公认的解决方案更好。我只能根据已经提供的信息走这么远,但如果 jolynice 愿意合作,也许我们可以学到一些东西。 :-)
所以...这是解决方案的初始镜头,如果返回更多信息,我将对其进行编辑。
题中原解出错是因为不满足泛型方法ReadFileCsv<T, Tmap>(...)
中的约束。我们不知道它们是什么,但从错误来看它们包括 T : class
和 Tmap : class
。因此,正确答案的第一步是在测试方法本身上重现被调用方法的所有约束。
更新:此代码实际上不起作用。简而言之,我在本地有这个功能,我认为它已经被添加到 NUnit 但它没有。另请参阅下面的更新文本...
[TestCase(typeof(CalendarGeneralCsv), typeof(CalendarGeneralCsvMap), 121)]
public void ReadFromCsvFileWithConfigurationMapTest<T,Tmap>(int totalRowsExptected)
where T : class
where Tmap : class
{
//Arrange
//Act
var records = csvService.ReadFileCsv<T, Tmap>(_csvToRead, ",") as IEnumerable<object>;
var result = new List<object>(records);
//Assert
result.Should().NotBeNullOrEmpty();
result.Should().HaveCount(totalRowsExptected);
}
更新以回应@Jannes 的评论
您可以在 C# 中创建不带参数的泛型方法。如果您使用这样的方法作为测试方法,NUnit 将需要知道用于调用它的实际类型。很遗憾,没有这样的方法。
目前,NUnit 只能从您提供的参数中推断出实际类型。这意味着泛型方法的每个参数类型必须至少有一个参数。
这显然是 NUnit 中的一个漏洞,并且在 GitHub 的各种问题中都对此进行了讨论。到目前为止,还没有任何提案被接受。例如,请参阅 https://github.com/nunit/nunit/issues 上的问题 150、1215、2562 和 3576。