突变测试是评估单元测试质量的一种方法。 对于突变测试, 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:突变 文档。
增量改进
如果在更改代码后,单元测试成功通过,那么这些测试就不够严谨,并且变异体幸存下来。 突变测试后,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 的可能性。