3

我正在阅读这个主题,它是关于使用反射来测试私有变量......

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

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

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

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

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

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

这是一个好的做法还是坏的做法?

4

5 回答 5

1

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

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

在过去,我编写了一个扩展方法,可以满足您的描述:

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

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

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

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

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

于 2013-01-31T01:40:55.933 回答
0

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

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

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

于 2013-01-30T22:35:25.287 回答
0

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

反思真的应该只是最后的手段

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

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

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

于 2013-01-31T00:40:18.130 回答
0

我想说使用反射来进行简单的单元测试有很多正当的理由。引用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 时,比较对象图而不是单个字段。您知道所有字段都被覆盖,因为所有字段都已打印。

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

于 2013-01-30T22:03:53.620 回答