Per ogni sviluppatore è importante scrivere software di qualità, che si ottiene implementando buoni casi di test.
Oggigiorno esistono diverse librerie e framework che permettono di controllare la Code Coverage e la bontà dei nostri casi di test.
Per quest’ultimo aspetto esiste una tecnica di test chiamata Mutation Testing e ovviamente librerie che permettono di applicarla in diversi linguaggi di programmazione.
Durante l’unconference del camp aziendale del 29 ottobre 2019 Lorenzo Testa ha proposto una sessione introduttiva ad alcune soluzioni per linguaggi Java e JavaScript.
Misurare la Code Coverage con JaCoCo
JaCoCo è una libreria per il Code Coverage in ambito Java.
Derivato dal plugin per Eclipse EclEmma, JaCoCo ha come obiettivi principali leggerezza e flessibilità, essendo capace di integrarsi con numerosi strumenti: Ant, Sonar e anche Maven.
É disponibile come plugin per IDE come Eclipse e IntelliJ.
Lorenzo ha mostrato il funzionamento di questa libreria in una suite di casi di test con il framework JUnit 5.
L’esecuzione del test tramite JUnit mette automaticamente in moto l’agente JaCoCo il quale crea un coverage report in formato binario nella directory di destinazione target/jacoco.exec.
L’output sarà interpretato da altri strumenti e plugin quali Sonar Qube.
É’ possibile generare report leggibili in diversi formati come ad esempio HTML, CSV e XML. Per dimostrazione Lorenzo ha mostrato il report in formato HTML generato nella directory target/site/jacoco/index.html.
Report con JaCoCo
Un report JaCoCo aiuta ad analizzare visivamente la Code Coverage usando diamanti colorati per branch e colori di sfondo per le linee:
- il diamante rosso indica che non sono stati raggiunti branch durante la fase di test;
- con il diamante giallo si indica il codice parzialmente coperto – alcuni branch non sono stati esercitati;
- infine con il diamante verde si notifica che tutti i branch sono stati esercitati durante il test.
JaCoCo fornisce principalmente tre parametri importanti:
- line Coverage che riflette la quantità di codice che è stata esercitata in base al numero di istruzioni Java chiamate dai test;
- branch Coverage che mostra la percentuale di branch esercitate nel codice, generalmente correlata alle istruzioni if / else e switch;
- complessità ciclomatica che indica la complessità del codice fornendo il numero di percorsi necessari per coprire tutti i possibili percorsi in un codice attraverso una combinazione lineare.
Mutation testing in Java con PIT Mutation Testing
La coverage con test tradizionali (righe, istruzioni, branch, ecc.) misurano solo il codice che viene eseguito dai test. Non verifica che i test siano effettivamente in grado di rilevare bug nel codice eseguito. Identificano quindi solo il codice che non è stato testato.
Gli esempi più estremi del problema sono i test senza asserzioni. Fortunatamente sono rari nella maggior parte delle code base. Molto più comune è invece il codice che viene testato solo parzialmente dalla sua suite. Una suite che verifica solo parzialmente il codice può comunque eseguire tutti i suoi rami.
Poiché è effettivamente in grado di rilevare se ogni affermazione è testata in modo significativo, il Mutation Testing è il gold standard rispetto al quale vengono misurati tutti gli altri tipi di coverage.
Si consideri il seguente codice:
if (a && b) { c = 1; } else { c = 0; }
Chiunque penserà che vadano scritti test per soddisfare tutti i percorsi possibili. Che succede qualora la condizione cambi in or, cioè (a || b)? I nostri test considerano questo caso?
Mutazione e mutanti
La mutazione di un test consiste nella modifica di una piccola parte dello stesso, in modo che si possa osservare il comportamento del software in questa nuova casistica.
Ciò che deve accadere è che:
- in un test si deve raggiunge l’istruzione mutata;
- l’input deve infettare l’istruzione mutata;
- l’output deve essere rilevabile dal test.
Un mutante può avere i seguenti stati:
- ucciso: se almeno un test fallisce mentre il mutante è attivo, il mutante è ucciso (comportamento ottimale);
- sopravvissuto: i test hanno esito positivo mentre il mutante è attivo;
- no coverage: non ci sono test che passano dall’istruzione mutata;
- timeout: i test vanno in timeout mentre il mutante è attivo;
- runtime error: occorre un errore a runtime nel test.
Se il mutante non viene ucciso ma il comportamento del programma è invariato, si è in presenza di mutanti equivalenti.
Il test viene eseguito generando le mutazioni da una serie di operatori di mutazione i quali, applicandoli al codice, uno alla volta ne cambiano il comportamento; se uno dei test mutati fallisce, allora il mutante è stato ucciso.
Con questa tecnica viene automaticamente ampliata la casistica di test alla quale il software è sottoposto e per questo motivo si può associare il concetto di Code Coverage del requisito con maggior aderenza.
Il rapporto tra mutanti uccisi e mutanti sopravvissuti per ogni suite di test dà una misura, in termini di Code Coverage, del requisito.
PITest e Stryker
Fortunatamente esistono librerie, pensate per diversi linguaggi di programmazione, che fanno il cosiddetto lavoro sporco, ovvero generare i mutanti per i casi di test da noi scritti.
Per il mondo Java esiste PITest, e Lorenzo ne ha mostrato l’utilizzo e soprattutto l’esecuzione che come risultato produce un report HTML con le informazioni su Code Coverage e mutanti.
Nell’ultima parte dell’intervento, Lorenzo ha mostrato l’equivalente PITest per JavaScript, la libreria Stryker.
Per quanto riguarda la demo di JaCoCo e PITest, è stato utilizzato un progettino Java che implementa il gioco del FizzBuzz (link al repo nel paragrafo Riferimenti), mentre per la demo di Stryker è stata utilizzato il codice presente nella pagina di documentazione della libreria.
Riferimenti
- Complessità ciclomatica
- JaCoCo
- PIT Mutation Testing
- Stryker Mutation Testing
- Repo GitHub del FizzBuzz con JaCoCo e PITest