Поделиться через


Тестирование мутаций

Тестирование мутаций — это способ оценить качество наших модульных тестов. Для тестирования мутаций средство 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 вносит намеренно. Идея проста: если ваши тесты хороши, они должны обнаружить изменения и провалиться. Если они по-прежнему проходят, ваши тесты могут быть недостаточно эффективными.

В нашем примере мутант — это замена выражения price <= 0на price < 0, например, после чего выполняются модульные тесты.

Stryker поддерживает несколько типов мутаций:

Тип Описание
Эквивалент Эквивалентный оператор используется для замены оператора его эквивалентом. Например, x < y преобразуется в x <= y.
Арифметика Арифметический оператор используется для замены арифметического оператора эквивалентом. Например, x + y преобразуется в x - y.
Струна Оператор строки используется для замены строки его эквивалентом. Например, "text" преобразуется в "".
Логический Логический оператор используется для замены логического оператора эквивалентом. Например, x && y преобразуется в x \|\| y.

Дополнительные типы мутаций см. в документации Stryker.NET: Mutations.

Постепенное улучшение

Если после изменения кода модульные тесты проходят успешно, они недостаточно надежны, и мутант выжил. После тестирования мутаций пять мутантов выжили.

Давайте добавим тестовые данные для значений границ и снова запустите тестирование мутаций.

[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

Тестирование мутаций помогает найти возможности для улучшения тестов, которые делают их более надежными. Это заставляет вас проверять не только «счастливый путь», но и сложные пограничные случаи, снижая вероятность ошибок в продакшене.