突变测试

突变测试是评估单元测试质量的一种方法。 对于突变测试, 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 控制台报告

Stryker.NET 将详细的 HTML 报告保存在 StrykerOutput 目录中。

Stryker 第一个报表

现在,考虑什么是变异体,什么是“幸存”和“被杀”的含义。 变异体是 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);
}

Stryker 第二份报告

如你所见,更正等价变异体后,我们只剩下字符串变异,通过检查异常消息的文本,我们可以轻松地“消除”它们。

[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);
}

Stryker 最终报告

突变测试有助于找到改进使测试更可靠的机会。 它强制你不仅检查“理想路径”,还要检查复杂的边界情况,从而降低在生产环境中出现 bug 的可能性。