C# 中的变异测试:破坏代码以使其更好

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

当测试通过,但代码仍然“损坏”时

\n\n

想象一下这种情况:您为 C# 项目编写了单元测试,dotnet test 显示绿灯,代码覆盖率达到 85%。团队很满意,CI/CD 完美运行。但一个月后,生产环境中出现了一个本应由单元测试捕获的 bug。熟悉吗?

\n\n

问题在于,代码覆盖率是一个具有欺骗性的指标。它显示了哪些代码行被触及,但并未检查测试本身的质量。您可能拥有 100% 的覆盖率,却没有一个有意义的断言。正是在这里,C# 中的变异测试登场了——这是一种通过“破坏”您的代码来检验您的测试捕获错误能力的方法。

\n\n

在本文中,我们将探讨什么是变异测试,如何使用 Stryker.NET 将其集成到 .NET 项目中,以及为什么它对于任何严肃的 C# 开发者来说都是必备技能。

\n\n

什么是变异测试?

\n\n

变异测试是一种评估测试质量的方法,它基于对程序源代码进行微小、有意的更改(变异)。每个这样的修改后的代码版本被称为变异体。

\n\n

逻辑很简单:

\n
    \n
  • 获取您的生产代码。
  • \n
  • 自动生成变异体——例如,将运算符 + 替换为 -,将条件 if (x > 5) 改为 if (x >= 5),将 true 改为 false。
  • \n
  • 针对每个变异体运行您的单元测试。
  • \n
  • 如果测试失败——变异体被“杀死”。这是一个好的测试。
  • \n
  • 如果测试通过——变异体“存活”。这是一个不好的迹象:您的测试没有注意到逻辑上的变化。
  • \n
\n\n

目标是实现高比例的变异体被杀死(通常 80% 以上)。这是您的测试在检验行为而不仅仅是“遍历”代码的真实保证。

\n\n

为什么代码覆盖率不够?

\n\n

考虑一个简单的 C# 示例。假设我们有一个检查数字是否为偶数的方法:

\n\n
public class MathUtils\n{\n    public bool IsEven(int number)\n    {\n        return number % 2 == 0;\n    }\n}
\n\n

以及一个具有 100% 覆盖率的“完美”测试:

\n\n
[Test]\npublic void IsEven_ShouldReturnTrueForEvenNumber()\n{\n    var utils = new MathUtils();\n    var result = utils.IsEven(4);\n    Assert.IsTrue(result);\n}
\n\n

测试通过,覆盖率 100%。但是,如果有人意外(或恶意)将方法改为 return number % 2 == 1; 会怎样?我们唯一的测试会失败吗?不会!它会检查数字 4(4 % 2 == 0,但现在方法返回 false),断言会失败——变异体将被杀死。一切顺利?等等。

\n\n

如果代码改为 return number % 2 != 1; 呢?逻辑上是一样的,但变异测试会创建一个变异体:return number % 2 != 0;。我们使用数字 4 的测试仍然会通过(4 % 2 != 0 为 false,方法返回 false,但断言期望 true——测试会失败)。这里没问题。但想象一下,我们这样编写测试:

\n\n
[Test]\npublic void IsEven_ShouldNotThrow()\n{\n    var utils = new MathUtils();\n    Assert.DoesNotThrow(() => utils.IsEven(4));\n}
\n\n

覆盖率仍然是 100%,但断言没有检查结果!现在任何变异体都会存活。变异测试会立即显示您的测试是空壳。

\n\n

C# 中的变异测试工具

\n\n

对于 .NET 来说,主要且实际上是标准的工具是 Stryker.NET。这是一个开源框架,可以轻松集成到任何 C# 项目中。它支持 xUnit、NUnit 和 MSTest。

\n\n

其他工具(不太流行):

\n
    \n
  • VisualMutator — 一个过时的 Visual Studio 插件。
  • \n
  • Nester

博客

书籍推荐