突变测试示例:从脆弱的 TDD 演变而来

对于交付完全符合预期的精简代码而言,测试驱动开发还不够。突变测试是向前迈出的有力一步。以下是它的样子。
130 位读者喜欢这篇文章。

本系列第三篇文章演示了如何使用失败和单元测试来开发更好的代码。

虽然看似借助一个成功的物联网 (IoT) 应用来控制猫门,旅程已经结束,但经验丰富的程序员知道解决方案需要突变

什么是突变测试?

突变测试是迭代遍历已实现代码的每一行,突变该行,然后运行单元测试并检查突变是否破坏了预期。如果未破坏,则您已创建了一个幸存的突变体。

幸存的突变体始终是一个令人担忧的问题,它指向代码库中潜在的风险区域。一旦您捕获到幸存的突变体,就必须杀死它。杀死幸存突变体的唯一方法是创建额外的描述——描述您对函数或模块输出的期望的新单元测试。最后,您交付一个精简、高效的解决方案,该解决方案是密封的,并保证代码库中没有潜伏的讨厌的错误或缺陷。

如果您让幸存的突变体闲逛、繁殖、长寿和繁荣,那么您就是在制造令人恐惧的技术债务。另一方面,如果任何单元测试抱怨临时突变的代码行产生的输出与预期输出不同,则突变体已被杀死。

安装 Stryker

尝试突变测试的最快方法是利用专用框架。此示例使用 Stryker

要安装 Stryker,请转到命令行并运行

$ dotnet tool install -g dotnet-stryker

要运行 Stryker,请导航到 unittest 文件夹并键入

$ dotnet-stryker

这是 Stryker 关于我们解决方案质量的报告

14 mutants have been created. Each mutant will now be tested, this could take a while.

Tests progress | 14/14 | 100% | ~0m 00s | 
Killed : 13
Survived : 1
Timeout : 0

All mutants have been tested, and your mutation score has been calculated 
- \app [13/14 (92.86%)]
[...]

报告显示

  • Stryker 创建了 14 个突变体
  • Stryker 看到 13 个突变体被单元测试杀死
  • Stryker 看到一个突变体在单元测试的猛攻中幸存下来
  • Stryker 计算出,现有代码库包含 92.86% 的代码用于满足预期
  • Stryker 计算出,代码库中 7.14% 的代码不符合预期

总的来说,Stryker 声称在本系列前三篇文章中组装的应用未能产生可靠的解决方案。

如何杀死突变体

当软件开发人员遇到幸存的突变体时,他们通常会求助于已实现的代码,并寻找修改它的方法。例如,在猫门自动化示例应用中,更改行

string trapDoorStatus = "Undetermined";

string trapDoorStatus = "";

并再次运行 Stryker。一个突变体幸存下来

All mutants have been tested, and your mutation score has been calculated
- \app [13/14 (92.86%)]
[...]
[Survived] String mutation on line 4: '""' ==> '"Stryker was here!"'
[...]

这次,您可以看到 Stryker 突变了行

string trapDoorStatus = "";

string trapDoorStatus = ""Stryker was here!";

这是一个 Stryker 如何工作的绝佳示例:它以一种巧妙的方式突变我们代码的每一行,以查看我们是否还有其他测试用例尚未考虑。它迫使我们更深入地考虑我们的期望。

被 Stryker 击败后,您可以尝试通过向其中添加更多逻辑来改进已实现的代码

public string Control(string dayOrNight) {
   string trapDoorStatus = "Undetermined";
   if(dayOrNight == "Nighttime") {
       trapDoorStatus = "Cat trap door disabled";
   } else if(dayOrNight == "Daylight") {
       trapDoorStatus = "Cat trap door enabled";
   } else {
       trapDoorStatus = "Undetermined";
   }
   return trapDoorStatus;
}

但是在再次运行 Stryker 后,您会看到此尝试创建了一个新的突变体

ll mutants have been tested, and your mutation score has been calculated
- \app [13/15 (86.67%)]
[...]
[Survived] String mutation on line 4: '"Undetermined"' ==> '""'
[...]
[Survived] String mutation on line 10: '"Undetermined"' ==> '""'
[...]

Stryker report

您无法通过修改已实现的代码来摆脱这种困境。事实证明,杀死幸存突变体的唯一方法是描述额外的期望。您如何描述期望?通过编写单元测试。

用于成功的单元测试

现在是时候添加新的单元测试了。由于幸存的突变体位于第 4 行,因此您意识到您尚未指定值“Undetermined”的输出期望。

让我们添加一个新的单元测试

[Fact]
public void GivenIncorrectTimeOfDayReturnUndetermined() {
   var expected = "Undetermined";
   var actual = catTrapDoor.Control("Incorrect input");
   Assert.Equal(expected, actual);
}

修复有效!现在所有突变体都被杀死了

All mutants have been tested, and your mutation score has been calculated
- \app [14/14 (100%)]
[Killed] [...]

您最终得到了一个完整的解决方案,包括对系统收到不正确的输入值时预期输出的描述。

突变测试来救援

假设您决定过度设计解决方案,并将此方法添加到 FakeCatTrapDoor

private string getTrapDoorStatus(string dayOrNight) {
   string status = "Everything okay";
   if(dayOrNight != "Nighttime" || dayOrNight != "Daylight") {
       status = "Undetermined";
   }
   return status;
}

然后将第 4 行语句替换为

string trapDoorStatus = "Undetermined";

string trapDoorStatus = getTrapDoorStatus(dayOrNight);

当您运行单元测试时,一切都通过了

Starting test execution, please wait...

Total tests: 5. Passed: 5. Failed: 0. Skipped: 0.
Test Run Successful.
Test execution time: 2.7191 Seconds

测试已通过,没有问题。TDD 奏效了。但是将 Stryker 带到现场,突然间情况看起来有点严峻

All mutants have been tested, and your mutation score has been calculated
- \app [14/20 (70%)]
[...]

Stryker 创建了 20 个突变体;14 个突变体被杀死,而 6 个突变体幸存下来。这使成功率降至 70%。这意味着我们只有 70% 的代码是为了满足描述的期望而存在的。另外 30% 的代码的存在没有明确的原因,这使我们面临滥用该代码的风险。

在这种情况下,Stryker 有助于对抗臃肿。它不鼓励使用不必要且复杂的逻辑,因为错误和缺陷滋生于这种不必要的复杂逻辑的裂缝中。

结论

正如您所见,突变测试确保没有不确定的事实未经检查。

您可以将 Stryker 比作一位国际象棋大师,他正在思考所有可能的获胜步骤。当 Stryker 不确定时,它是在告诉您获胜还不是保证。我们记录的事实单元测试越多,我们在比赛中就走得越远,Stryker 就越有可能预测胜利。在任何情况下,即使表面上一切看起来都不错,Stryker 也有助于检测失败的情况。

正确地设计代码始终是一个好主意。您已经看到了 TDD 如何在这方面提供帮助。TDD 在保持代码高度模块化方面尤其有用。但是,仅靠 TDD 对于交付完全符合预期的精简代码是不够的。开发人员可以在没有首先描述期望的情况下向已实现的代码库添加代码。这使整个代码库处于风险之中。突变测试在捕获常规测试驱动开发 (TDD) 节奏中的漏洞方面尤其有用。您需要突变已实现代码的每一行,以确保没有一行代码的存在没有特定原因。

既然您了解了突变测试的工作原理,就应该研究如何利用它。下次,我将向您展示如何在处理更复杂的场景时充分利用突变测试。我还将介绍更多敏捷概念,以了解 DevOps 文化如何从成熟的技术中受益。

接下来阅读什么
标签
User profile image.
Alex 自 1990 年以来一直从事软件开发。他目前的热情是如何将“软”重新带回软件中。他坚信,我们的行业已经达到了可以完全实现这个崇高目标(即将“软”重新带回软件中)的成熟度。

2 条评论

非常有趣,感谢分享。

感谢分享

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.