使用反射来进行单元测试的复杂对象断言是不好的做法吗?

时间:2013-01-30 21:33:46

标签: c# unit-testing reflection

我正在阅读this topic,这是关于使用反射来测试私有变量......

但我的单元测试中没有这样的问题,我的代码完全可以测试。

唯一的问题是我想通了,在对具有预期结果的复杂对象的每个属性进行断言时非常耗时;特别是对于复杂对象的列表。

由于它是一个复杂的对象,除非我为每个对象实现Assert.AreEqual,否则正常IEquality不会给我一个正确的结果。

但即使我这样做,也不会告诉我在断言期间哪个属性/字段的名称,期望值和实际值。

正确地说,我们手动将每个属性值放入一个列表并执行单个CollectionAssertion,但这仍然非常耗时,并且当断言发生时它只告诉我元素值的索引是不平等它不会告诉我属性名称。这使得调试变得非常困难(我必须进入调试模式并查看集合中的元素)。

所以我想知道,如果我编写一个递归反射方法,它将对两个复杂对象进行断言,这将告诉我每个属性名称,期望值,实际值。

这是一种好习惯还是坏习惯?

5 个答案:

答案 0 :(得分:1)

我发现很多人甚至不会考虑反思,但它有它的位置。它在性能,类型安全等方面肯定存在缺陷,正如其他海报所述,但我认为单元测试是一个很好的使用场所。只要它成功完成。

当您不拥有属性中使用的所有类型时,尝试在所有对象上强制执行相等实现会遇到问题。实现一百个迷你比较器类与手动写出断言一样耗时。

在过去,我编写了一个扩展方法来执行您所描述的内容:

  • 比较两个相同类型的对象(或实现通用接口的对象)
  • 反射用于查找所有公共属性。
  • 如果属性是值类型,则直接执行Assert.AreEquals
  • 对于参考类型,它会进行递归调用

我的测试在任何时候都不关心属性名称,因此对重命名进行重构无关紧要。实际上,会自动找到新属性,而忘记删除的属性。

我从来没有将它用于非常复杂的物体,但它与我拥有的物体一起运行良好而不会减慢我的测试速度。

所以我认为在单元测试中可以随意使用Reflection。

编辑:我会尝试为你挖掘我的方法。

答案 1 :(得分:0)

在我看来,使用Reflection并不是一个很好的选择。使用Reflection意味着我们在编译时失去了类型安全性。而且,在使用Reflection之后,可能(通常)通过程序集的元数据进行不区分大小写的字符串搜索。这导致性能下降。考虑到这些方面,我认为拆分原始类型(如oleksii所推荐)是一种很好的方法。

另一种方法可能是使用纯访问器方法编写单独的测试,以测试单独的属性集。这可能不适用于所有情况。但是,在某些情况下确实如此。

例如:如果我有一个Customer类,我可以写一个测试来检查Address-type字段;我可以编写另一个测试来检查订单类型字段等等。

答案 2 :(得分:0)

正常情况下,您不应该需要反射来执行任何单元测试相关的操作。在回答您链接的问题时提到了这一点:

  

反思应该只是最后的手段

如果需要检查复杂对象是否相等,在单元测试中实现此类等式检查。为单元测试目的而准备额外的代码没有错:

public void ComplexObjectsAreEqual()
{
    var first = // ...
    var second = // ...

    AssertComplexObjectsAreEqual(first, second);
}

private void AssertComplexObjectsAreEqual(ComplexObject first,
    ComplexObject second)
{
    Assert.That(first.Property1, Is.EqualTo(second.Property1),
       "Property1 differs: {0} vs {1}", first.Property1, second.Property1); 
    // ...
}

您不应该将单元测试视为其他一些代码。如果需要编写某些东西以使它们更具可读性,清洁,可维护 - 写下来。它与其他地方的代码相同。您会通过生产代码中的反射来比较对象吗?

答案 3 :(得分:0)

我想说使用反射进行简单的单元测试有很多正当理由。引用https://github.com/kbilsted/StatePrinter

手动单元测试的问题

很费劲。

当我一遍又一遍地打字并重新输入时:Assert.This,Assert。那,......不禁想知道为什么计算机无法为我自动化这些东西。所有那些不必要的打字需要时间并耗尽我的精力。

使用Stateprinter时,只要预期值与实际值不匹配,就会为您生成断言。

代码和测试不同步

当代码更改时,例如通过向类添加字段,您需要在某些测试中添加断言。但是,找到一个完全手动的过程。在没有人对所有类进行全面概述的较大项目中,所需的更改不会在所有应该执行的地方执行。

将代码从一个分支合并到另一个分支时会出现类似的情况。假设您将发布分支中的错误修复或功能合并到开发分支,我一遍又一遍地观察到代码被合并,所有测试都运行然后提交合并。人们忘记重新访问并仔细检查整个测试套件,以确定开发分支上存在测试,而不是合并发生的分支上的测试,相应地调整这些测试。

使用Stateprinter时,会比较对象图而不是单个字段。因此,当创建新字段时,所有相关测试都会失败。您可以将打印调整到特定字段,但是您无法自动检测图表中的更改。

可读性差I

您可以通过测试类,测试方法和测试元素的标准命名的良好命名来实现这一目标。但是,没有命名约定可以弥补断言创建的视觉混乱。当索引用于从列表或词典中挑选元素时,会添加进一步的混乱。并且在将它与for,foreach循环或LINQ表达式结合使用时不要让我开始。

使用StatePrinter时,会比较对象图而不是单个字段。因此,测试中不需要逻辑来挑选数据。

可读性差II

当我阅读如下的测试时。想想这里真正重要的是什么

Assert.IsNotNull(result, "result");
Assert.IsNotNull(result.VersionData, "Version data");
CollectionAssert.IsNotEmpty(result.VersionData)
var adjustmentAccountsInfoData = result.VersionData[0].AdjustmentAccountsInfo;
Assert.IsFalse(adjustmentAccountsInfoData.IsContractAssociatedWithAScheme);
Assert.AreEqual(RiskGroupStatus.High, adjustmentAccountsInfoData.Status);
Assert.That(adjustmentAccountsInfoData.RiskGroupModel, Is.EqualTo(RiskGroupModel.Flexible));
Assert.AreEqual("b", adjustmentAccountsInfoData.PriceModel);
Assert.IsTrue(adjustmentAccountsInfoData.IsManual);

当真正蒸馏时我们想要表达的是

adjustmentAccountsInfoData.IsContractAssociatedWithAScheme = false
adjustmentAccountsInfoData.Status = RiskGroupStatus.High
adjustmentAccountsInfoData.RiskGroupModel = RiskGroupModel.Flexible
adjustmentAccountsInfoData.PriceModel = "b"
adjustmentAccountsInfoData.IsManual = true

可疑性差

当业务对象的字段数量增加时,相反的情况适用于测试的可靠性。是否覆盖了所有领域?字段是否被错误地多次比较?还是反对错误的领域?当你必须在一个对象上做25个断言时,你就会知道痛苦,并且精心确保在正确的字段中检查正确的字段。然后审稿人必须经历相同的练习。为什么不自动化?

使用StatePrinter时,会比较对象图而不是单个字段。您知道所有字段都已覆盖,因为所有字段都已打印。

答案 4 :(得分:-1)

恕我直言,这是一个不好的做法,因为:

  • 反思代码很慢,很难正确编写
  • 维护起来更加困难,而且这些代码可能不是重构友好的
  • 反射很慢,单元测试应该很快
  • 它也感觉正确

对我而言,这看起来好像是在插一个洞,而不是解决问题。为了解决这个问题,我可以建议将一个大而复杂的类分成一组较小的类。如果您有许多属性 - 将它们分组为单独的类


这样的类

class Foo
{
    T1 Prop1 {get; set;}
    T2 Prop2 {get; set;}
    T3 Prop3 {get; set;}
    T4 Prop4 {get; set;}
}

会变成

class Foo
{
    T12 Prop12 {get; set;}  
    T34 Prop34 {get; set;}  
}
class T12
{
    T1 Prop1 {get; set;}
    T2 Prop2 {get; set;}
}
class T34
{
    T3 Prop3 {get; set;}
    T4 Prop4 {get; set;}
}

注意,Foo现在只有一个属性(即“分组”表示)。如果您可以以某种方式对属性进行分组,那么任何状态更改都将本地化为特定组 - 您的任务将变得更加简化。然后,您可以断言“分组”属性等于预期状态。