在编程中,特别是在TDD中,有一些好的原则值得您遵循:DRY和通过公共方法进行测试。 他们在实践中反复证明自己,但是在具有大量旧代码的项目中,它们可能具有“黑暗面”。 例如,您可以按照这些原则编写代码,然后发现自己分解的测试覆盖了20多个抽象,其配置比被测逻辑大得多。 这种“阴暗面”使人们感到恐惧,并限制了TDD在项目中的使用。 在削减的范围内,我讨论了为什么通过公共方法进行测试很糟糕,以及如何减少由于该原理而引起的问题。
免责声明我想立即消除可能的印象。 有些人甚至可能没有感觉到将要讨论的缺点,例如由于其项目的规模。 而且,我认为这些缺点是技术债务的一部分,并且具有相同的特征:如果不引起注意,问题将会加剧。 因此,有必要根据情况做出决定。
原理的基础听起来不错:您需要测试行为,而不是实现。 这意味着您只需要测试类接口。 实际上,情况并非总是如此。 为了说明问题的实质,假设您有一种计算轮班工人成本的方法。 当涉及轮班工作时,这是一项艰巨的任务,因为 他们有小费,奖金,周末,节假日,公司规定等。此方法在内部执行许多操作,并使用其他服务提供有关节假日,小费等的信息。 在为其编写单元测试时,如果测试的代码位于方法末尾,则必须为所使用的所有服务创建配置。 同时,经过测试的代码本身只能部分使用,或者根本不使用可配置服务。 并且已经有一些以此方式编写的单元测试。
减号1:单元测试过度配置
现在,您要对具有非平凡逻辑的新功能添加反应,该新功能也将在方法末尾使用。 标志的性质使得它是服务逻辑的一部分,同时又不是服务接口的一部分。 在上述情况下,此代码仅与该公共方法相关,并且通常可以记录在旧方法内部。
如果项目采用了仅通过公共方法测试所有内容的规则,则开发人员很可能可以复制一些现有的单元测试并对其进行一些调整。 在新测试中,仍将配置所有服务以运行该方法。 一方面,我们遵守了该原理,但是另一方面,我们获得了带有过度配置的单元测试。 将来,如果发生故障或需要更改配置,您将不得不做猴子工作来调整测试。 这很乏味,漫长,并且不会给客户带来欢乐或明显的好处。 似乎我们遵循的是正确的原则,但是我们处于我们想要摆脱的那种情况下,拒绝测试私有方法。
缺点2:覆盖范围不完整
此外,诸如懒惰的人为因素可能会介入。 例如,具有非平凡标志逻辑的私有方法在此示例中可能看起来像。
private bool HasShifts(DateTime date, int tolerance, bool clockIn, Shift[] shifts, int[] locationIds) { bool isInLimit(DateTime date1, DateTime date2, int limit) => Math.Abs(date2.Subtract(date1).TotalMinutes) <= limit; var shiftsOfLocations = shifts.Where(x => locationIds.Contains(x.LocationId)); return clockIn ? shiftsOfLocations.Any(x => isInLimit(date, x.StartDate, tolerance)) : shiftsOfLocations.Any(x => isInLimit(date, x.EndDate, tolerance)); }
此方法需要进行10次检查才能涵盖所有情况,其中8次是有意义的。
解码8个重要案例- shiftsOfLocations-2个值-是否
- clockIn-2个值-true或false
- 公差-2种不同的含义
总计:2 x 2 x 2 = 8
在编写单元测试以测试此逻辑时,开发人员将必须编写至少8个大型单元测试。 我遇到了一些案例,其中单元测试配置占用了50多行代码,其中有4行直接调用。 即 只有大约10%的代码携带有效载荷。 在这种情况下,通过编写较少的单元测试来减少工作量的诱惑很大。 结果,例如,对于每个clockIn值,在8个中,仅剩下两个单元测试。 这种情况导致这样的事实,或者编写所有必要的测试,创建配置(Ctrl + C,V可以运行,如果没有它的话)既繁琐又费时,或者该方法仅被部分覆盖。 每个选项都有其不愉快的后果。
可能的解决方案
除了“测试行为”原则外,还有OCP(开放/封闭原则)。 正确应用它,您会忘记什么是“易碎性测试”,而无法测试模块的内部行为。 如果需要新的模块行为,则将为新的后继类编写新的单元测试,在其中将更改所需的行为。 这样,您就不必花费时间在重新检查和更新现有测试上。 在这种情况下,可以将此方法声明为内部方法,也可以将其声明为受保护的内部方法,并通过将InternalsVisibleTo添加到程序集进行测试。 在这种情况下,您的IClass接口将不会受到影响,并且测试将是最简单的,不会经常更改。
另一种选择是声明一个附加的帮助程序类,可以通过将其声明为公共方法来将其拉入其中。 然后将遵循该原理,并且测试将变得简洁。 我认为,这种方法并不总能奏效。 例如,有些人可能决定甚至将一个方法拉入一个类,这导致使用一个方法创建一堆类。 另一个极端是将此类方法转储到一个帮助器类中,该类将成为GOD-helper类。 但是,如果正在工作的程序集使用强名称签名,并且由于某种原因而您无法对测试程序集进行签名,则带有帮助器的此选项可能是唯一的选择。 当两个程序集都签名或不签名时,InternalsVisibleTo将起作用。
总结
最后,由于这些问题的结合,TDD和单元测试的想法受到了影响,因为 没有人希望编写体积测试并提供支持。 我将很高兴看到任何示例,说明严格遵守该原则如何导致问题以及减少了开发团队编写测试的动力。