Jak ulepszyć jakość kodu? Czyli trochę o testach mutacyjnych

Czym są testy mutacyjne?

Testy mutacyjne polegają na wprowadzaniu sztucznych zmian (mutantów) do kodu źródłowego w celu sprawdzenia, czy istniejące testy jednostkowe są wystarczająco dokładne i potrafią wykryć błędy. Mutacje te symulują potencjalne błędy, które programiści mogą popełnić podczas pisania kodu.

Po co używać testy mutacyjne?

Testy mutacyjne pozwalają weryfikować testy jednostkowe np. sprawdzając czy nie brakuje jakiś przypadków testowych. Jeśli testy jednostkowe nie są wystarczająco skuteczne w wykrywaniu mutacji, wtedy mutanty pozostaną i oznacza to, że nasze testy jednostkowe nie są wystarczające.

Przykład praktyczny

Do tego przykładu wykorzystałem .NET w wersji 8 oraz bibliotekę Stryker.Net

Zacznijmy od stworzenia klasy, niech to będzie jedna prosta klasa i metoda.

public class FinancialComponent
{
    public decimal CalculateInterest(decimal principal, decimal rate)
    {
        if (principal <= 0 || rate <= 0)
        {
            return -1;
        }

        return principal * rate / 25;
    }
}

Do tego dodajmy jakieś testy jednostkowe.

[TestFixture]
public class FinancialComponentTests
{
    [Test]
    public void CalculateInterest_ValidInput_ReturnsInterest()
    {
        // Arrange
        var financialComponent = new FinancialComponent();

        // Act
        var result = financialComponent.CalculateInterest(1000, 5);

        // Assert
        Assert.AreEqual(200, result);
    }

    [Test]
    public void CalculateInterest_InvalidInput_ReturnsMinusOne()
    {
        // Arrange
        var financialComponent = new FinancialComponent();

        // Act
        var result = financialComponent.CalculateInterest(-1000, 5);

        // Assert
        Assert.AreEqual(-1, result);
    }
}

Dobra. Widzimy, że mamy metodę CalculateInterest która na podstawie otrzymanych parametrów oblicza odsetki. Do tego mamy dwa proste testy, które sprawdzą sprawdzają czy po podaniu jakiś parametrów otrzymamy oczekiwany rezultat.
Pierwszy test wprowadza poprawne parametry i sprawdza czy odsetki są dobrze wyliczone. Drugi sprawdza czy po podaniu minusowej wartości otrzymamy oczekiwane -1.
Okej na pierwszy rzut oka wygląda dobrze, niby przechodzimy całe flow metody. No ale jednak nie do końca. Chociaż testy jednostkowe mówią co innego.

Uruchomię teraz testy mutacyjne

O co tutaj chodzi? Bardzo proste. Mamy informację na temat ile mutantów udało nam się zabić a ile przetrwało. „Zabijamy” mutant poprzez dobrze przygotowane unit testy. Mutant tworzy się poprzez zmutowanie kodu produkcyjnego – czyli jego zmianę. W tym przypadku w klasie FinancialComponent.cs
Na przykład taki mutant:

Teraz wiemy, że jeśli doszłoby do pomyłki podczas pisania kodu i zmienilibyśmy znak, nasze testy by tego nie wyłapały. Dodajmy więc kilka testów sprawdzających naszego if’ka

[TestCase(-1000, 5)]
[TestCase(0, 5)]
[TestCase(1000, 0)]
[TestCase(1000, -5)]
public void CalculateInterest_InvalidInput_ReturnsMinusOne(decimal principal, decimal rate)
{
    // Arrange
    var financialComponent = new FinancialComponent();

    // Act
    var result = financialComponent.CalculateInterest(principal, rate);

    // Assert
    Assert.AreEqual(-1, result);
}

Sprawdziłem dodatkowe warianty naszego warunku. Teraz uruchomię ponownie testy mutacyjne

I proszę, pozbyliśmy się wszystkich mutantów


To tyle, dzięki. Mam nadzieję, że w jakiś sposób było to dla Ciebie przydatne.

Jeśli masz jakieś pytania pytaj śmiało. Możesz odwiedzić mojego instagrama o tutaj. Pokazuje tam tego co aktualnie się uczę i różne inne rzeczy. Zapraszam!

Scroll to Top