【问题标题】:Unit Test with Generics types具有泛型类型的单元测试
【发布时间】:2019-01-05 01:24:46
【问题描述】:

我想用泛型测试一个单元测试,我正在努力寻找正确的方法。

我有这个

[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必须是引用类型。

【问题讨论】:

    标签: c# unit-testing generics testing nunit


    【解决方案1】:

    如果已经有几个答案并且一个被接受,我通常不会回复,但它们似乎都是基于测试方法不能通用的假设。他们绝对可以。我的记忆告诉我这曾经有很好的记录,但它似乎不再存在了——或者我的记忆错了——这解释了为什么你可能认为它不可能。

    通用解决方案在这里可能不是最好的,但尝试起来似乎是一件有趣的事情,并且可能会更好,或者阐明为什么公认的解决方案更好。我只能使用已经提供的信息,但如果 jolynice 会合作,也许我们可以学到一些东西。 :-)

    所以...这是解决方案的初步尝试,如果有更多信息回来,我将对其进行编辑。

    问题中的原始解决方案导致错误,因为不满足泛型方法ReadFileCsv&lt;T, Tmap&gt;(...) 中的约束。我们不知道它们是什么,但从错误中它们包括T : classTmap : 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。

    【讨论】:

    • 你确定这是正确的吗?我不使用 NUnit,但在 XUnit 中,与 TestCase 属性等效的参数将是正在使用的泛型类型的实例。所以像这样:[TestCase(new CalendarGeneralCsv(), new CalendarGeneralCsvMap(), 121)]
    • 你好查理,感谢你参与这个问题。 ;) ,我尝试了你的解决方案,我仍然有参考。错误 CS0311 类型“Tmap”不能用作泛型类型或方法“CsvServices.ReadFileCsv(string, string)”中的类型参数“TMap”。没有从“Tmap”到“CsvHelper.Configuration.ClassMap”的隐式引用转换
    • jolynice:更新了代码示例。如果您看到更多错误,请发布方法 ReadFileCsv 的声明(即标头)。
    • 也许我做错了什么,但这似乎不起作用(不再?)。简单地导致“为没有参数的方法提供的参数”。您的答案实际上是互联网上唯一提到这个结构的地方。
    • @Jannes 是的,没错。我编写了代码来处理初始类型参数,我认为它已发布,但显然没有。这是我在 2019 年初回应这个问题时的印象。据我所知,那个错误的答案在我改变之前已经存在了大约 11 个小时。我错过了什么?我在任何地方都找不到您引用的评论。
    【解决方案2】:

    或者,如果您不打算在具有不同类型的测试中使用多个 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);
    }
    

    但是,如果您确实想为多个类型/值运行相同的测试用例,您可以将实际的测试逻辑提取到通用方法中。然后,您可以为要测试的每组数据创建一个新测试,将类型显式传递给泛型方法:

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

    【讨论】:

    • 你好 devNull,你给的解决方案,我已经实现了,但是我需要测试多个泛型类型。无论如何感谢您的回答。
    • @jolynice 我已经用另一个选项更新了我的答案,允许您为同一个测试用例编写多个测试,为每个测试传递不同的类型/值。
    【解决方案3】:

    虽然已经有一个被接受的答案(注意:自从我最初发布以来它已经改变了),我想提供一种使用反射的替代方法。将测试方法拆分为两种方法,跳板方法通用测试方法。有几个优点:

    • 通用测试方法看起来更像任何其他测试方法。它没有混入无关的反射。

    • 通用的测试方法可以正常单步执行,也是因为它没有混入无关的反射。

    • 对被测组件的更改更有可能在测试项目中触发编译器错误,因此您知道通用测试方法(可能还有跳板方法)需要更新。另外,由于抛出异常的位置,在运行时更清楚这是因为支持反射而不是组件的使用方式。

    • 跳板方法不需要知道被测组件的任何信息,只需要知道如何调用通用测试方法即可。

    • 图案可以轻松且一致地复制,因为变化非常小。

    这是一个基于问题和接受的答案的示例:

    [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(),因为它正在寻找相同类型(测试类)的方法。这样可以减少变化,从而可以更轻松地复制图案。

    通用测试方法有一个不同的名称(不管它是什么,只要它不同),因此GetMethod 调用不需要指定参数类型。该名称应该只有一个方法并且它是公共的,因此它也不需要BindingFlags。或者,您可以将其设为私有,只需添加 BindingFlags.NonPublic | BindingFlags.Instance注意:并非所有框架版本都具有采用BindingFlags 的重载。如果您想将其设为私有,则必须找到替代方案。

    通用测试方法需要包含约束。这使得约束成为测试的正式部分。如果不满足约束,反射将在运行时失败,但是将它们放在通用测试方法上,您可能会从一开始就编写更好的测试。您提到TTmap 必须是引用类型,因此它们包含在上面。

    最后,您的跳板能够定义多个测试用例,正如您所指出的那样,您需要能够这样做,所以我在上面添加了另一个日历和映射。

    【讨论】:

    • 您好 madreflection、devNull、DavidG、Charlie 和 Nikosi,感谢大家提供的所有解决方案。我非常感谢这个社区。​​span>
    【解决方案4】:

    考虑使用反射来调用被测的通用主题

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

    【讨论】:

    • 哇,Nikosi,太好了,它工作正常。喜欢你的解释,通俗易懂。 5开始。我只是更改 Invoke 参数。第二个参数是它的 object[] 参数,所以我创建了 this 。 var records = genericMethod.Invoke(csvService, new object[] { _csvToRead, "," }) as IEnumerable;
    • 您的回答解决了问题,但您能否删除或编辑第一行?我可以想象这个答案被引用为证明 NUnit 中没有通用测试方法。肯定有,但我不确定这是否是这里最好的解决方案......不过,为了好玩,我会看看我是否可以写下一个。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-19
    • 1970-01-01
    • 1970-01-01
    • 2020-08-26
    • 2015-07-09
    相关资源
    最近更新 更多