Test delle mutazioni

Il test delle mutazioni è un modo per valutare la qualità degli unit test. Per il test delle mutazioni, lo strumento Stryker.NET esegue automaticamente le mutazioni nel codice, esegue i test e genera un report dettagliato con i risultati.

Scenario di test di esempio

Si consideri un esempio PriceCalculator.cs classe con un Calculate metodo che calcola il prezzo, tenendo conto dello sconto.

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

Il metodo precedente è coperto dai test unitari seguenti:

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

Il codice precedente evidenzia due progetti, uno per il servizio che funge da e PriceCalculator l'altro è il progetto di test.

Installare lo strumento globale

Prima di tutto, installare Stryker.NET. A tale scopo, è necessario eseguire il comando :

dotnet tool install -g dotnet-stryker

Per eseguire stryker, richiamarlo dalla riga di comando nella directory in cui si trova il progetto di unit test:

dotnet stryker

Dopo l'esecuzione dei test, nella console viene visualizzato un report.

Report della console Stryker

Stryker.NET salva un report HTML dettagliato nella directory StrykerOutput.

Primo report di Stryker

Ora, considera cosa sono i mutanti e cosa significa "sopravvissuto" e "ucciso". Un mutante è una piccola modifica nel codice che Stryker apporta a scopo. L'idea è semplice: se i test sono validi, dovrebbero rilevare la modifica e fallire. Se vengono ancora superati, i test potrebbero non essere abbastanza forti.

In questo esempio, un mutante sarà la sostituzione dell'espressione price <= 0, ad esempio con price < 0, dopo il quale vengono eseguiti unit test.

Stryker supporta diversi tipi di mutazioni:

TIPO Descrizione
Equivalente L'operatore equivalente viene usato per sostituire un operatore con il relativo equivalente. Ad esempio, x < y diventa x <= y.
Aritmetica L'operatore aritmetico viene usato per sostituire un operatore aritmetico con il relativo equivalente. Ad esempio, x + y diventa x - y.
Stringa L'operatore string viene usato per sostituire una stringa con il relativo equivalente. Ad esempio, "text" diventa "".
Logico L'operatore logico viene usato per sostituire un operatore logico con il relativo equivalente. Ad esempio, x && y diventa x \|\| y.

Per altri tipi di mutazione, vedere la documentazione Stryker.NET: Mutazioni .

Interpretare i risultati dei test delle mutazioni

Dopo aver eseguito Stryker.NET, si riceverà un report che classifica i mutanti come uccisi, sopravvissuti o timeout. Ecco come interpretare e agire su questi risultati:

  • Eliminate: Si tratta di modifiche che i tuoi test hanno scoperto con successo. Un numero elevato di mutanti uccisi indica che il gruppo di test rileva in modo efficace gli errori logici.
  • Sopravvissuto: queste modifiche non sono state rilevate dai test. Esaminarli per identificare le lacune nella copertura dei test o nelle asserzioni troppo deboli. Concentrarsi sull'aggiunta di unit test mirati che fallirebbero se il mutante fosse reale.
  • Timeout: queste mutazioni hanno causato al codice il blocco o il superamento del tempo consentito. Ciò può verificarsi con cicli infiniti o percorsi non ottimizzati. Esaminare la logica del codice o aumentare la soglia di timeout, se necessario.

Annotazioni

Non cercare di ottenere un punteggio di mutazione del 100%. Invece, concentrarsi su aree ad alto rischio o business critical in cui i bug non rilevati sarebbero più costosi.

Aggiunta di test di mutazione al flusso di lavoro CI/CD

È possibile integrare facilmente i test delle mutazioni nei flussi di lavoro di integrazione e recapito continui. Ad esempio, Stryker.NET può essere configurato per l'esecuzione all'interno della configurazione di Azure Pipelines o GitHub Actions, consentendo di applicare soglie di qualità come parte del processo di test automatizzato.

Personalizzazione

Oltre a impostare soglie per la pipeline, Stryker.NET offre la possibilità di avere configurazioni diverse per ognuna delle esigenze del progetto. È possibile personalizzare il comportamento usando il file stryker-config.json .

{
  "stryker-config": {
    "ignore-mutations": [
      "string",
      "logical"
    ],
    "ignore-methods": [
      "*Logs"
    ],
    "mutate": [
      "!**/Migrations/*",
      "!**/*.Designer.cs"
    ]
  }
}
  • ignore-mutazioni: i tipi di mutazione da escludere dai test perché sono rumorosi o non rilevanti per la logica dell'applicazione. Verranno visualizzati nei report come Ignored.
  • ignore-methods: Puoi usarlo per saltare interi metodi in base alle loro signature. Questi elementi vengono visualizzati anche nei report come Ignored. Nell'esempio precedente tutti i metodi che terminano in "Logs" vengono ignorati.
  • mutate: senza questa opzione, Stryker tenterà di modificare tutti i file nel progetto. A questo scopo, è possibile ignorare file o intere cartelle. Nell'esempio precedente tutti gli elementi all'interno di una cartella Migrations e di tutti . Designer.cs file (che in genere vengono generati automaticamente) vengono ignorati.

Per altre informazioni, vedere Stryker: Configuration.

Miglioramento incrementale

Se, dopo aver modificato il codice, i test unitari vengono superati con successo, allora non sono sufficientemente robusti e il mutante è sopravvissuto. Dopo il test delle mutazioni, cinque mutanti sopravvivono.

Aggiungere i dati di test per i valori limite ed eseguire di nuovo il test delle mutazioni.

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

Secondo rapporto Stryker

Come si può notare, dopo aver corretto i mutanti equivalenti, abbiamo solo mutazioni di stringa lasciate, che possiamo facilmente "uccidere" controllando il testo del messaggio di eccezione.

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

Relazione finale di Stryker

I test di mutazione consentono di trovare opportunità per migliorare i test che li rendono più affidabili. Ti costringe a verificare non solo il "caso di successo", ma anche casi limite complessi, riducendo la probabilità di bug nell'ambiente di produzione.