突变测试是评估单元测试质量的一种方法。 对于突变测试, Stryker.NET 工具会自动在代码中执行突变,运行测试,并生成包含结果的详细报告。
示例测试方案
考虑一个示例 PriceCalculator.cs 类,其中包含一个 Calculate 计算价格的方法,并考虑折扣。
public class PriceCalculator
{
public decimal CalculatePrice(decimal price, decimal discountPercent)
{
if (price <= 0)
{
throw new ArgumentException("Price must be greater than zero.");
}
if (discountPercent < 0 || discountPercent > 100)
{
throw new ArgumentException("Discount percent must be between 0 and 100.");
}
var discount = price * (discountPercent / 100);
var discountedPrice = price - discount;
return Math.Round(discountedPrice, 2);
}
}
上述方法由以下单元测试涵盖:
[Fact]
public void ApplyDiscountCorrectly()
{
decimal price = 100;
decimal discountPercent = 10;
var calculator = new PriceCalculator();
var result = calculator.CalculatePrice(price, discountPercent);
Assert.Equal(90.00m, result);
}
[Fact]
public void InvalidDiscountPercent_ShouldThrowException()
{
var calculator = new PriceCalculator();
Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(100, -1));
Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(100, 101));
}
[Fact]
public void InvalidPrice_ShouldThrowException()
{
var calculator = new PriceCalculator();
Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(-10, 10));
}
前面的代码突出显示了两个项目,一个项目用于充当服务 PriceCalculator ,另一个项目是测试项目。
安装全局工具
首先,安装 Stryker.NET。 为此,需要执行以下命令:
dotnet tool install -g dotnet-stryker
若要运行 stryker,请从单元测试项目所在的目录中的命令行调用它:
dotnet stryker
测试运行后,控制台中会显示一个报表。
Stryker.NET 将详细的 HTML 报告保存在 StrykerOutput 目录中。
现在,考虑什么是变异体,什么是“幸存”和“被杀”的含义。 变异体是 Stryker 有意在你的代码中进行的小更改。 这个想法很简单:如果你的测试做得很好,它们就应该能捕捉到变化并使其失败。 如果它们仍然通过,则说明你的测试可能不够严格。
在我们的示例中,变异体是指将表达式 price <= 0 替换为 price < 0,例如,然后运行单元测试。
Stryker 支持多种类型的突变:
| 类型 | DESCRIPTION |
|---|---|
| 等效 | 等效运算符用于将运算符替换为其等效运算符。 例如,x < y 将变为 x <= y。 |
| 算术 | 算术运算符用于将一个算术运算符替换为相应的等效运算符。 例如,x + y 将变为 x - y。 |
| 字符串 | 字符串运算符用于将字符串替换为其等效项。 例如,"text" 将变为 ""。 |
| 逻辑 | 逻辑运算符用于将逻辑运算符替换为其等效运算符。 例如,x && y 将变为 x \|\| y。 |
有关其他突变类型,请参阅 Stryker.NET:突变 文档。
解释突变测试结果
运行 Stryker.NET 后,你将收到一份报告,该报告将变种分类为“被杀死”、“幸存”或“超时”。 下面介绍如何解释和处理这些结果:
- 已终止:这些更改是测试成功捕获的更改。 大量被“杀死”的变异体表示您的测试套件能够有效地检测逻辑错误。
- 幸存下来:测试未捕获这些更改。 查看它们以识别测试覆盖率的不足或断言过于薄弱的地方。 专注于添加目标单元测试,如果变异体是真实的,则会失败。
- 超时:这些突变导致代码挂起或超过允许的时间。 这可能发生在无限循环或未优化的路径中。 调查代码逻辑,或根据需要增加超时阈值。
注释
不要追求达到 100% 的突变分数。 而是专注于高风险或关键业务领域,其中未检测到的错误可能导致最高成本。
将突变测试添加到 CI/CD 工作流
可以将突变测试无缝集成到持续集成和交付工作流中。 例如,可以将 Stryker.NET 配置为在 Azure Pipelines 或 GitHub Actions 设置中运行,从而允许在自动化测试过程中强制实施质量阈值。
自定义
除了为管道设置阈值外,Stryker.NET 还提供针对每个项目需求的不同配置的可能性。 可以使用 stryker-config.json 文件自定义行为。
{
"stryker-config": {
"ignore-mutations": [
"string",
"logical"
],
"ignore-methods": [
"*Logs"
],
"mutate": [
"!**/Migrations/*",
"!**/*.Designer.cs"
]
}
}
-
ignore-mutations:要从测试中排除的突变类型,因为它们与应用程序逻辑无关或干扰。 它们将显示在你的报告中。
Ignored -
ignore-methods:可以使用此功能来根据其签名跳过整个方法。 这些内容也会在报表中显示为
Ignored。 在前面的示例中,将忽略以“Logs”结尾的所有方法。 - 变异:如果没有此选项,Stryker 将尝试变异项目中的所有文件。 因此,可以忽略文件或整个文件夹。 在前面的示例中,所有 Migrations 文件夹内的内容和所有 .Designer.cs 文件(这些文件通常是自动生成的)都被忽略。
有关详细信息,请参阅 Stryker:配置。
增量改进
如果在更改代码后,单元测试成功通过,那么这些测试就不够严谨,并且变异体幸存下来。 突变测试后,5种变异体幸存下来。
让我们为边界值添加测试数据,并再次运行突变测试。
[Fact]
public void InvalidPrice_ShouldThrowException()
{
var calculator = new PriceCalculator();
// changed price from -10 to 0
Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(0, 10));
}
[Fact] // Added test for 0 and 100 discount
public void NoExceptionForZeroAnd100Discount()
{
var calculator = new PriceCalculator();
var exceptionWhen0 = Record.Exception(() => calculator.CalculatePrice(100, 0));
var exceptionWhen100 = Record.Exception(() => calculator.CalculatePrice(100, 100));
Assert.Null(exceptionWhen0);
Assert.Null(exceptionWhen100);
}
如你所见,更正等价变异体后,我们只剩下字符串变异,通过检查异常消息的文本,我们可以轻松地“消除”它们。
[Fact]
public void InvalidDiscountPercent_ShouldThrowExceptionWithCorrectMessage()
{
var calculator = new PriceCalculator();
var ex1 = Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(100, -1));
Assert.Equal("Discount percent must be between 0 and 100.", ex1.Message);
var ex2 = Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(100, 101));
Assert.Equal("Discount percent must be between 0 and 100.", ex2.Message);
}
[Fact]
public void InvalidPrice_ShouldThrowExceptionWithCorrectMessage()
{
var calculator = new PriceCalculator();
var ex = Assert.Throws<ArgumentException>(() => calculator.CalculatePrice(0, 10));
Assert.Equal("Price must be greater than zero.", ex.Message);
}
突变测试有助于找到改进使测试更可靠的机会。 它强制你不仅检查“理想路径”,还要检查复杂的边界情况,从而降低在生产环境中出现 bug 的可能性。