我们都参加过——甚至可能发表过——演讲者特别迷恋某种语言或工具的演讲,并且在诸如“简单地添加此配置键或依赖项”之类的短语中过于频繁地使用该词。总是推荐一些健康的怀疑来抵消这种热情,特别是当技术是新的、很少使用的、专业的或三者的组合时。
软件框架永远不会免费工作,即使您不支付许可费。不熟悉的技术有一个学习曲线,无论多么温和。不要告诉我我可以通过“仅”添加一行build.gradle(或二十到pom.xml)来获得工具 X 的所有好处。特别是应始终将测试工具视为潜在的责任。他们必须通过相应的质量提高来证明其总拥有成本是合理的,而这种更高的质量必须具有商业意义。抱歉打消了你的热情,但你没有被聘为艺术家。这样的等式是不可能用硬数字来表达的。常识必须足够。
基于属性的测试和突变测试
在这篇文章中,我想讨论 Java 测试工具包中两种成熟但相对小众的技术:使用 Pitest 进行突变测试和使用Jqwik进行基于属性的测试。我之前写过关于PBT和 MT 的文章,带有开发人员的帽子和“为技术而技术”的心态。现在,我将戴上 CFO 的预算批准帽,并解释为什么在使用它们之前应该三思而后行。但首先,如果您不熟悉这些技术,请进行非常简洁的复习。
突变测试 (MT) 框架对测试中的编译代码进行小而重要的更改(突变)。不用担心,这是在内存中完成的,不会触及源。JVM 仍然可以运行新的字节码,但如果您有足够的覆盖范围,更改后的行为现在应该会导致至少一个单元测试失败。我们称之为杀死突变体,用游戏玩家的话说。当测试套件的覆盖率很高但断言很差时,MT 尤其具有启发性。许多测试将保持绿色,但 MT 保持绿色是个坏消息。
基于属性的测试 (PBT) 完全不同。它允许您为属性定义测试场景,这些属性是适用于一系列值的真实语句。“未满 18 岁者不得入内”就是这样的说法。使用 PBT boolean isAgeAllowed(int age),可以使用介于 0 到 18 之间的随机值范围调用某些方法。有关示例,请参见配套 GitLab 项目中的AdmissionCalculatorPropertySuite 。
MT 是一种质量验证技术,可在现有测试中发现缺失或不完整的断言。PBT 通过从多个角度打击现有测试来增强现有测试。在单元/组件/集成/端到端测试的测试金字塔谱中,它们位于底部。尽管它们存在差异,但它们有一个重要的共同点:它们是可以使结构良好的代码库变得更好的工具,但在测试成熟度较低的项目中,它们是无用的,即使不是真正有害的。此外,它们对于有效部署和使用也很重要。
让我们用一个好的测试驱动方法的典型例子来详细说明。您正在编写一个组件,该组件根据顾客的出生日期计算入场费。幸运的是,您的团队非常注重规范:
- 一个人的年龄必须评估为非负值。带有“出生日期无效”的信号例外。
- 4 岁以下的儿童或 90 岁以上的成人不得参加这个可怕的主题公园游乐设施。带有“顾客必须在 4 到 90 岁之间”的信号例外。
- 15 岁或以下的入场费为 10 欧元,16 岁及以上的入场费为 15 欧元。
该代码是一组简单的整数值 if 语句(请原谅我的冗长)。完整代码在这里。
爪哇1 如果(年龄 < 0){2 throw new IllegalArgumentException ( "出生日期 [%s] 无效" . formatted ( dateOfBirth ));3 } else if (年龄 < 4 || 年龄 > 90 ) {4 throw new IllegalArgumentException ( "顾客必须在 4 到 90 岁之间,但是是 [%s]" .formatted ( age ) ); 5 }否则 如果(年龄 < 16){6 返回 10 ;7 }其他{8 返回 15 ;9 }
使用像这样的简单代码很容易实现 100% 坚如磐石的覆盖率。请参阅入学计算器套件。对业务规则的大多数更改都会自动导致测试失败,但不是全部。让我们引入一个新规则。
65 岁或以上的成年人支付 10 欧元。
因此,儿童和老年人有资格享受折扣。就代码而言:if (age < 16)变为if (age < 16 || age >= 65)
您所有的单元测试仍然通过。类、方法、行甚至分支覆盖率仍然是 100%。测试说明了真相,但它不再是全部真相,因为在值 65 附近引入了一个新的、未经测试的边缘情况。如果您的工作是测试驱动的,那么您应该在添加新条件之前编写额外的测试场景。
当突变测试无济于事时
MT能抓住这个遗漏吗?是的,它可能已更改age >= 65为age > 65并提醒您没有测试来涵盖这种边缘情况。但是在实施更改时,您可以而且应该注意到它。您可以正确地争辩说,生产代码从来没有像这个例子那样微不足道。当您继承大型代码库时,MT 肯定更有助于提高测试质量吗?以下是我可能不是的原因。
- 当测试套件的断言很差时,您不需要 MT 告诉您。在 src/test/java 中对 'assert' 进行全文搜索会告诉您您需要知道的一切。
- 您不需要 MT 来检查测试覆盖率。有更有效的工具可以做到这一点。如果发现大部分代码,首先,MT 无法产生任何有用的东西,因为没有测试代码可以杀死突变体。
- 像 Pitest 这样的工具可以生成非常精确但也很详细的关于潜入雷达的突变体的报告。如果你有很好的覆盖范围但断言很差,这将是巨大的。这就像一个所有警告灯同时闪烁的 747 驾驶舱。知道首先要解决哪个问题需要判断。杀死所有的变种人是没有意义的,因为 100% 的测试覆盖率通常是不值得的。
- MT 框架为每个突变体多次运行相同的测试场景,因此被测代码应该快速执行。访问数据库、文件系统或网络会使突变测试运行速度慢得令人无法接受。同样,具有长方法和高圈复杂度的非内聚代码创造了许多引入突变的机会。相同的 long 方法将被调用无数次。
用 PBT 捕捉未知的边缘情况
PBT 在捕捉未经测试的业务逻辑添加方面做得很好。单元测试给你真相,但属性测试给你全部真相。由于它验证了 4 到 90 之间的所有值,因此它现在会在达到 65 到 90 的范围时失败。
爪哇1@财产2公共 voidany_age_between_four_and_ninety_is_valid ( @ForAll @IntRange ( min = 4 , max = 90 ) int age ) {3 断言(getAdmissionForAge(年龄))。是积极的();4}
从表面上看,上面看起来像是一个参数化的测试
爪哇1@ParameterizedTest2@ValueSource ( ints = { 4 , 90 })3公共 无效
any_age_between_four_and_ninety_is_valid(整数 年龄){4... }5
不过,不要被愚弄。上面的单元测试不测试“任何年龄”,只测试我们碰巧知道的边缘情况。使用 PBT 作为一种霰弹枪方法来杀死你忘记的边缘情况是很诱人的,但这与它的精神背道而驰。您应该从记录的属性开始,并将它们转换为可运行的测试用例。指定这些属性应该在测试和生产代码之前。
没有框架可以挽救低标准的测试
当测试成熟度较差的团队编写单元测试时,通常是为了确认生产代码的作用。给定值 X 和 Y,被测方法返回 Z,这就是我们所断言的。用一千个不同的值(如 PBT 所做的那样)来打击它似乎毫无意义。如果你的自动化测试只是像那样巩固现状,那确实是毫无意义的。您可以从这种方法中获得的最好的结果是对回归的一些保护。PBT 和 MT 都不会帮助您。他们无法揭示实施中的逻辑判断失误,更不用说解释设计时的错误了。一开始可能没有。
MT 和 PBT 在经过良好测试的关键业务代码中具有价值,其中充满了 if 语句、开关和(数字)边缘情况,您需要金钱可以买到的所有稳健性。相反,如果一个方法对任何浮点值的行为都是可预测的,那么用随机输入对其进行一千次测试并不会给你太多的洞察力。
不要将这些框架用于支持功能,即支持应用程序算法核心的代码:Web 或消息传递端点、数据库访问层、安全层或数据传输映射逻辑。不要编写遇到此类代码的 PBT 场景,并确保 Pitest 忽略这些部分进行突变。
这些资源密集型框架只有在您将显着算法正确地隔离为可以测试数千次而不会破坏房屋的小类时才值得。当您将逻辑重构为可测试的块并提高单元测试的覆盖率和质量时,您甚至可能会发现不再需要突变测试。
PBT 和 MT 是令人着迷的技术,所以一定要看看它们。但它们也是硕士论文的内容。他们对他们有一点学术气息,脱离了质量承担成本并且必须可以协商的商业世界。如果您决定使用它们,请花时间充分了解它们,不要陷入为测试而测试的心态。