【问题标题】:Is it bad practice to use reflection to do complex object assertion for a unit test?使用反射为单元测试进行复杂的对象断言是不好的做法吗?
【发布时间】:2013-01-14 20:55:50
【问题描述】:

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

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

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

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

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

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

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

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

【问题讨论】:

  • 我会说不好-您使测试与被测内容的内部关系过于密切。如果您需要IEquality 语义,请实现它。
  • 我很确定至少有一个流行的框架支持基于反射的断言(例如提供两个 POCO).. stackoverflow.com/a/7440471/166390 (FluentAssertions) , stackoverflow.com/questions/318210/… (impl) , stackoverflow.com/a/2047576/166390 (xUnit)
  • @Oded 但使用 IEquality 它不会告诉我在进行断言时究竟在哪里失败。即 ClassA:IEquality,ClassA 是一个复杂对象,具有属性 ClassB AnotherObj,而 ClassB 有一个值 Property,导致 Equals 失败。在断言失败的情况下,我将不得不遍历整个 ClassA + ClassB 的所有属性才能找到它失败的确切位置......
  • @pst 是的,我也刚读过 xUnit。但如果它们是坏的,那它们为什么存在呢?
  • @KingChan 有些人发布东西只是为了发布东西!

标签: c# unit-testing reflection


【解决方案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 现在只有一个属性(即“分组”表示)。如果您可以以某种方式对属性进行分组,以便将任何状态更改本地化到特定组 - 您的任务会变得更加简化。然后,您可以断言“分组”属性等于预期状态。

【讨论】:

  • 当这些工具使用动态代理或生成表达式树时,只要不访问私有字段,实际上可以很快。
  • 动态代码通常比静态代码慢,而且不是类型安全的。如果更改属性的名称,表达式树可能会变得难以重构。
  • 我同意反射速度较慢,但​​我不必对这种方法做太多维护。该方法针对所有公共属性/字段运行,每当找到复杂的对象/集合时递归调用。对象模型中的任何更改都不会影响方法运行,新属性将自动在方法中获取。就像上面描述的 Mike Parkhill 一样。
  • 而且我们的模型是Web API模型,所以我不能改变模型:(
【解决方案2】:

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

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

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

【讨论】:

  • hmm 你能给我一个使用这种断言方法的类型安全的例子吗?因为基本上方法本身应该检查两个对象是否为同一类型,因此它们的属性将是相同的。并且在对每个属性进行断言时,需要检查两个属性的值(并且属性类型相同)。
  • @KingChan:我指的是对属性名称或类型名称的更改。而且我不知道您的程序集的结构。我想,如果使用不同的工厂方法来生成这 2 个对象,那么使用反射可能意味着我们要执行更多检查。
【解决方案3】:

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

反思应该只是最后的手段

如果需要检查复杂对象是否相等,在单元测试中实现这样的相等检查。纯粹出于单元测试目的而拥有额外的代码并没有错:

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

您不应该真正将单元测试视为其他代码。如果需要编写某些内容以使它们更具可读性、简洁性、可维护性——那就写吧。它与其他地方的代码相同。您会在生产代码中通过反射来比较对象吗?

【讨论】:

    【解决方案4】:

    我发现很多人甚至不会考虑反射,但它有它的位置。正如其他海报所说,它在性能、类型安全等方面肯定有缺点,但我实际上认为单元测试是使用它的好地方。只要做得恰到好处。

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

    过去我写过一个扩展方法,可以满足你的描述:

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

    我的测试从不关心属性名称,因此重构重命名并不重要。事实上,新属性会自动找到,而删除的属性会被遗忘。

    我从未将它用于真正复杂的对象,但它与我所拥有的对象一起工作得很好,而不会减慢我的测试速度。

    所以在我看来,在单元测试中请谨慎使用反射。

    编辑:我会尽力为你挖掘我的方法。

    【讨论】:

      【解决方案5】:

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

      手动单元测试的问题

      很费力。

      当我一遍又一遍地键入和重新键入时:Assert.This、Assert.That、... 不禁想知道为什么计算机不能为我自动执行这些操作。所有这些不必要的打字都需要时间并消耗我的精力。

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

      代码和测试不同步

      当代码发生变化时,比如向类中添加字段,您需要在某些测试中添加断言。但是,找到位置是一个完全手动的过程。在没有人全面了解所有类的大型项目中,所需的更改并没有在所有应该执行的地方进行。

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

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

      可读性差我

      您在测试类、测试方法的良好命名和测试元素的标准命名方面取得了长足的进步。但是,没有任何命名约定可以弥补断言造成的视觉混乱。当使用索引从列表或字典中挑选元素时,会更加混乱。在将它与 for、foreach 循环或 LINQ 表达式结合使用时,不要让我开始。

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

      可读性差二

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

      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 时,比较对象图而不是单个字段。您知道所有字段都被覆盖,因为所有字段都已打印。

      【讨论】:

        猜你喜欢
        • 2011-02-18
        • 2010-10-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-01-22
        • 1970-01-01
        • 2014-03-11
        • 2013-10-16
        相关资源
        最近更新 更多