Is it bad practice to use reflection to do complex object assertion for a unit test?
我正在阅读这个主题,这是关于使用反射来测试私有变量...
但我的单元测试中没有这样的问题,我的代码完全可以测试。
唯一的问题是我想通了,对具有预期结果的复杂对象的每个属性进行断言时非常耗时;特别是对于复杂对象的列表。
由于它是一个复杂的对象,除非我为每个对象实现
但即使我这样做,也不会告诉我在断言期间哪个属性/字段的名称,期望值和实际值。
正确地说,我们手动将每个属性值放入一个列表并执行单个
所以我想知道,如果我编写一个递归反射方法,它将对两个复杂对象进行断言,它将告诉我每个属性名称,期望值,实际值。
这是一种好的做法还是不好的做法?
我发现很多人甚至不会考虑反思,但它有它的位置。它在性能,类型安全等方面肯定存在缺陷,正如其他海报所述,但我认为单元测试是一个很好的使用场所。只要它成功完成。
当您不拥有在属性中使用的所有类型时,尝试在所有对象上强制执行等同实现会进入墙。实现一百个迷你比较器类与手动写出断言一样耗时。
在过去,我编写了一个扩展方法,可以执行您所描述的操作:
- 比较两个相同类型的对象(或实现一个通用接口)
- 反射用于查找所有公共属性。
- 如果属性是值类型,则完成Assert.AreEquals
- 对于引用类型,它执行递归调用
我的测试在任何时候都不关心属性名称,因此重构重构并不重要。实际上,会自动找到新属性,而忘记删除的属性。
我从来没有将它用于非常复杂的物体,但是它与我拥有的物体配合得很好,而不会减慢我的测试速度。
所以我认为在单元测试中可以随意使用Reflection。
编辑:我会尝试为你挖掘我的方法。
我想说使用反射进行简单的单元测试有很多正当理由。引用https://github.com/kbilsted/StatePrinter
手动单元测试的问题
这很费劲。
当我一遍又一遍地打字并重新输入时:Assert.This,Assert。那,......不禁想知道为什么计算机无法为我自动化这些东西。所有那些不必要的打字需要时间并耗尽我的精力。
使用Stateprinter时,只要预期值和实际值不匹配,就会为您生成断言。
代码和测试不同步
当代码更改时,例如通过向类添加字段,您需要在某些测试中添加断言。但是,找到一个完全手动的过程。在没有人对所有类进行完整概述的较大项目中,所需的更改不会在所有应该执行的地方执行。
将代码从一个分支合并到另一个分支时会出现类似的情况。假设您将发布分支中的错误修复或功能合并到开发分支,我一遍又一遍地观察到代码被合并,所有测试都运行然后提交合并。人们忘记重新访问并仔细检查整个测试套件,以确定开发分支上是否存在测试,而不是合并发生的分支上的测试,相应地调整这些测试。
使用Stateprinter时,会比较对象图而不是单个字段。因此,当创建新字段时,所有相关测试都会失败。您可以将打印调整到特定字段,但是您无法自动检测图表中的更改。
可读性差我
通过对测试类,测试方法和测试元素的标准命名的良好命名,您可以获得很长的成功。但是,没有命名约定可以弥补断言创建的视觉混乱。当索引用于从列表或词典中挑选元素时,会添加进一步的混乱。并且在将它与for,foreach循环或LINQ表达式结合使用时,不要让我开始。
使用StatePrinter时,会比较对象图而不是单个字段。因此,测试中不需要逻辑来挑选数据。
可读性差二
当我读下面的测试时。想想这里真正重要的是什么
1 2 3 4 5 6 7 8 9 | 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); |
什么时候蒸馏我们想要表达的是什么
1 2 3 4 5 | adjustmentAccountsInfoData.IsContractAssociatedWithAScheme = false adjustmentAccountsInfoData.Status = RiskGroupStatus.High adjustmentAccountsInfoData.RiskGroupModel = RiskGroupModel.Flexible adjustmentAccountsInfoData.PriceModel ="b" adjustmentAccountsInfoData.IsManual = true |
可怜的可怜
当业务对象的字段数量增加时,相反的情况对于测试的可靠性来说也是如此。是否覆盖了所有领域?字段是否被错误地多次比较?还是反对错误的领域?当你必须在一个对象上做25个断言时,你就会知道痛苦,并且精心确保在正确的字段中检查正确的字段。然后审稿人必须经历相同的练习。为什么这不是自动化的?
使用StatePrinter时,会比较对象图而不是单个字段。您知道所有字段都已覆盖,因为所有字段都已打印。
在正常情况下,您不应该需要反射来做与测试相关的任何事情。在回答您链接的问题时提到了这一点:
Reflection should really only be a last resort
如果需要检查复杂对象是否相等,请在单元测试中实现此类等式检查。纯粹用于单元测试目的的额外代码没有错:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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); // ... } |
您不应该将单元测试视为其他代码。如果需要编写某些东西以使它们更具可读性,清洁,可维护 - 写下来。它与其他地方的代码相同。您是否会通过生产代码中的反射来比较对象?
在我看来,使用Reflection不是一个很好的选择。使用Reflection意味着我们在编译时失去了类型安全性。而且,在使用Reflection之后,可能(通常)通过程序集的元数据进行不区分大小写的字符串搜索。这导致性能下降。考虑到这些方面,我认为拆分原始类型(如oleksii所推荐)是一种很好的方法。
另一种方法可以是使用纯访问器方法编写单独的测试,以测试单独的属性集。这可能不适用于所有情况。但是,在某些情况下确实如此。
例如:如果我有一个Customer类,我可以编写一个测试来检查Address-type字段;我可以编写另一个测试来检查订单类型字段等等。
恕我直言,这是一个不好的做法,因为:
- 反射代码很慢,很难正确编写
- 维护起来更加困难,而且这些代码可能不是重构友好的
- 反射很慢,单元测试应该很快
- 它也感觉不对 sub>
对我来说,这看起来好像是在试图堵塞一个洞,而不是解决问题。为了解决这个问题,我可以建议将一个大而复杂的类分成一组较小的类。如果您有许多属性 - 将它们分组到单个类中
这样的课
1 2 3 4 5 6 7 | class Foo { T1 Prop1 {get; set;} T2 Prop2 {get; set;} T3 Prop3 {get; set;} T4 Prop4 {get; set;} } |
会成为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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;} } |
注意,