Test di mutazione con PITest

1. Panoramica

Il test del software si riferisce alle tecniche utilizzate per valutare la funzionalità di un'applicazione software. In questo articolo, discuteremo alcune delle metriche utilizzate nel settore dei test del software, come la copertura del codice e il test di mutazione , con particolare interesse su come eseguire un test di mutazione utilizzando la libreria PITest .

Per semplicità, baseremo questa dimostrazione su una funzione palindromo di base: si noti che un palindromo è una stringa che legge lo stesso avanti e indietro.

2. Dipendenze di Maven

Come puoi vedere nella configurazione delle dipendenze di Maven, useremo JUnit per eseguire i nostri test e la libreria PITest per introdurre mutanti nel nostro codice: non preoccuparti, vedremo tra un secondo cos'è un mutante. Puoi sempre cercare l'ultima versione della dipendenza nel repository centrale di Maven seguendo questo link.

 org.pitest pitest-parent 1.1.10 pom  

Per avere la libreria PITest attiva e funzionante, dobbiamo anche includere il plug-in pitest-maven nel nostro file di configurazione pom.xml :

 org.pitest pitest-maven 1.1.10   com.baeldung.testing.mutation.*   com.baeldung.mutation.test.*    

3. Configurazione del progetto

Ora che abbiamo configurato le nostre dipendenze Maven, diamo un'occhiata a questa funzione palindromo autoesplicativa:

public boolean isPalindrome(String inputString) { if (inputString.length() == 0) { return true; } else { char firstChar = inputString.charAt(0); char lastChar = inputString.charAt(inputString.length() - 1); String mid = inputString.substring(1, inputString.length() - 1); return (firstChar == lastChar) && isPalindrome(mid); } } 

Tutto ciò di cui abbiamo bisogno ora è un semplice test JUnit per assicurarci che la nostra implementazione funzioni nel modo desiderato:

@Test public void whenPalindrom_thenAccept() { Palindrome palindromeTester = new Palindrome(); assertTrue(palindromeTester.isPalindrome("noon")); } 

Fin qui tutto bene, siamo pronti per eseguire con successo il nostro test case come test JUnit.

Successivamente, in questo articolo, ci concentreremo sul codice e sulla copertura delle mutazioni utilizzando la libreria PITest.

4. Copertura del codice

La copertura del codice è stata ampiamente utilizzata nell'industria del software, per misurare quale percentuale dei percorsi di esecuzione è stata esercitata durante i test automatizzati.

Possiamo misurare l'effettiva copertura del codice in base ai percorsi di esecuzione utilizzando strumenti come Eclemma disponibili su Eclipse IDE.

Dopo aver eseguito TestPalindrome con la copertura del codice, possiamo facilmente ottenere un punteggio di copertura del 100% - Nota che isPalindrome è ricorsivo, quindi è abbastanza ovvio che il controllo della lunghezza dell'input vuoto sarà comunque coperto.

Sfortunatamente, le metriche di copertura del codice a volte possono essere piuttosto inefficaci , perché un punteggio di copertura del codice del 100% significa solo che tutte le linee sono state esercitate almeno una volta, ma non dice nulla sull'accuratezza dei test o sulla completezza dei casi d'uso , ed è per questo che il test di mutazione è davvero importante.

5. Copertura delle mutazioni

Il test di mutazione è una tecnica di test utilizzata per migliorare l'adeguatezza dei test e identificare i difetti nel codice. L'idea è di modificare dinamicamente il codice di produzione e far fallire i test.

I buoni test falliscono

Ogni cambiamento nel codice è chiamato mutante e si traduce in una versione alterata del programma, chiamata mutazione .

Diciamo che la mutazione viene uccisa se può causare un fallimento nei test. Diciamo anche che la mutazione è sopravvissuta se il mutante non ha potuto influenzare il comportamento dei test.

Ora eseguiamo il test utilizzando Maven, con l'opzione goal impostata su: org.pitest: pitest-maven: mutationCoverage .

Possiamo controllare i report in formato HTML nella directory target / pit-test / YYYYMMDDHMI :

  • Copertura della linea al 100%: 7/7
  • Copertura mutazione 63%: 5/8

Chiaramente, il nostro test copre tutti i percorsi di esecuzione, quindi il punteggio di copertura della linea è del 100%. D'altra parte, la libreria PITest ha introdotto 8 mutanti , 5 di loro sono stati uccisi - ha causato un fallimento - ma 3 sono sopravvissuti.

Possiamo controllare il rapporto com.baeldung.testing.mutation / Palindrome.java.html per maggiori dettagli sui mutanti creati:



Questi sono i mutatori attivi per impostazione predefinita quando si esegue un test di copertura delle mutazioni:

  • INCREMENTS_MUTATOR
  • VOID_METHOD_CALL_MUTATOR
  • RETURN_VALS_MUTATOR
  • MATH_MUTATOR
  • NEGATE_CONDITIONALS_MUTATOR
  • INVERT_NEGS_MUTATOR
  • CONDITIONALS_BOUNDARY_MUTATOR

Per maggiori dettagli sui mutatori PITest, puoi controllare il collegamento alla pagina della documentazione ufficiale .

Il nostro punteggio di copertura delle mutazioni riflette la mancanza di casi di test , poiché non possiamo assicurarci che la nostra funzione palindromo rifiuti gli input di stringhe non palindromiche e quasi palindromiche.

6. Migliora il punteggio di mutazione

Ora che sappiamo cos'è una mutazione, dobbiamo migliorare il nostro punteggio di mutazione uccidendo i mutanti sopravvissuti .

Prendiamo come esempio la prima mutazione - condizionale negata - sulla riga 6. Il mutante è sopravvissuto perché anche se cambiamo lo snippet di codice:

if (inputString.length() == 0) { return true; }

Per:

if (inputString.length() != 0) { return true; }

Il test passerà ed è per questo che la mutazione è sopravvissuta . L'idea è di implementare un nuovo test che fallirà, nel caso in cui il mutante venga introdotto . Lo stesso può essere fatto per i rimanenti mutanti.

@Test public void whenNotPalindrom_thanReject() { Palindrome palindromeTester = new Palindrome(); assertFalse(palindromeTester.isPalindrome("box")); } @Test public void whenNearPalindrom_thanReject() { Palindrome palindromeTester = new Palindrome(); assertFalse(palindromeTester.isPalindrome("neon")); }

Ora possiamo eseguire i nostri test utilizzando il plug-in di copertura delle mutazioni, per assicurarci che tutte le mutazioni siano state uccise , come possiamo vedere nel rapporto PITest generato nella directory di destinazione.

  • Copertura della linea al 100%: 7/7
  • Copertura del 100% delle mutazioni: 8/8

7. Configurazione dei test PITest

Mutation testing may be resources-extensive sometimes, so we need to put proper configuration in place to improve tests effectiveness. We can make use of the targetClasses tag, to define the list of classes to be mutated. Mutation testing cannot be applied to all classes in a real world project, as it will be time-consuming, and resource critical.

It is also important to define the mutators you plan to use during mutation testing, in order to minimize the computing resources needed to perform the tests:

  com.baeldung.testing.mutation.*   com.baeldung.mutation.test.*   CONSTRUCTOR_CALLS VOID_METHOD_CALLS RETURN_VALS NON_VOID_METHOD_CALLS  

Moreover, the PITest library offers a variety of options available to customize your testing strategies, you can specify the maximum number of mutants introduced by class using the maxMutationsPerClass option for example. More details about PITest options in the official Maven quickstart guide.

8. Conclusion

Note that code coverage is still an important metric, but sometimes it is not sufficient enough to guarantee a well-tested code. So in this article we've walked through mutation testing as a more sophisticated way to ensure tests quality and endorse test cases, using the PITest library.

Abbiamo anche visto come analizzare un report PITest di base migliorando al contempo il punteggio di copertura delle mutazioni .

Anche se il test di mutazione rivela difetti nel codice, dovrebbe essere usato con saggezza, perché è un processo estremamente costoso e che richiede tempo .

Puoi controllare gli esempi forniti in questo articolo nel progetto GitHub collegato .