Mutation & Regression Testing - Andrea Fornaia, Ph.D. Department of Mathematics and Computer Science - DMI Unict
←
→
Trascrizione del contenuto della pagina
Se il tuo browser non visualizza correttamente la pagina, ti preghiamo di leggere il contenuto della pagina quaggiù
Mutation & Regression Testing Andrea Fornaia, Ph.D. Department of Mathematics and Computer Science University of Catania Viale A.Doria, 6 - 95125 Catania Italy fornaia@dmi.unict.it https://www.dmi.unict.it/fornaia/ 1 A.A. 2020/2021
Qualità di una test suite • L’obiettivo di una test suite è quello di esercitare il comportamento di un’applicazione, distinguendo un comportamento anomalo (almeno uno dei test fallisce - fail) dal comportamento atteso (tutti i test superati - pass) • Quanti e quali test dovremmo aggiungere per avere una test suite efficace? • In teoria, il miglior modo per misurare la qualità di una test suite sarebbe di valutarne la capacità di individuare difetti reali (bug) nell’applicazione • “Il test di un programma può essere usato per mostrare la presenza di bug, ma mai per mostrare la loro assenza” – Dijkstra (1970) • Una delle metriche maggiormente usate per misurare la qualità di una test suite è la copertura del codice durante l’esecuzione dei test • Non è l’unica metrica di copertura, ma è certamente una delle più immediate ed usate, anche grazie alla presenza di numerosi tool integrati con gli IDE • Cobertura • JaCoCo • Codcov 2
Code Coverage • Class coverage: percentuale di classi coperte • Method coverage: percentuale di metodi coperti • Line coverage: percentuale di linee di codice coperte • Statement coverage: percentuale di istruzioni coperte • su una linea posso avere più di una istruzione • teoricamente più preciso, ma spesso usato come sinonimo di line coverage • JaCoCo conta le istruzioni coperte a livello di bytecode • Branch coverage: percentuale di rami di esecuzione coperti • Es. if (a && b) ha 2 rami da coprire: • (a && b) è vera • (a && b) e falsa • Condition coverage: percentuale di condizioni coperte • Es. if (a && b) ha 4 condizioni da coprire: • (a) è vera • (a) è falsa • (b) è vera • (b) è falsa • JaCoCo conta i branch coperti a livello di bytecode, è più quindi una condition coverage 3
Esempio code coverage con JaCoCo @Test public void shouldGiveOkWhenBothTrue() { assertEquals(”OK", foo(true,true)); } - Le istruzioni di foo() sono 10 (le altre 3 sono del costruttore) - La copertura degli statement è 10/10 - La copertura dei branch (a livello bytecode) è 2/4 - La copertura delle linee è 4/4 (nel sorgente) - La copertura dei metodi è 2/2 (per la classe corrente) - Cxty indica la complessità ciclomatica calcolata come: v(G) = B - D + 1 B: #branch (4, sempre a livello di bytecode) D: #decision_point (2, il numero di istruzioni decisionali) - Cxty serve ad indicare quanti test case sono necessari per coprire tutto il metodo (ne mancano 2 su 3) - Cxty ci suggerisce che mancano 2 test case per avere una copertura completa… 4
Esempio code coverage con JaCoCo @Test public void shouldGiveOkWhenBothTrue() { assertEquals(”OK", foo(true,true)); } @Test public void shouldGiveKoWhenBothFalse() { assertEquals(”KO", foo(false,false)); } - La branch coverage è ora 3/4 - Nota: non è 4/4 perché nell’AND quando a == false il valore di b non verrà valutato - A livello di bytecode lo si può notare facilmente: se il primo ifeq (4) è false si salta a (14), evitando il secondo ifeq (8) 5
Esempio code coverage con JaCoCo @Test public void shouldGiveOkWhenBothTrue() { assertEquals(”OK", foo(true,true)); } @Test public void shouldGiveKoWhenBothFalse() { assertEquals(”KO", foo(false,false)); } @Test public void shouldGiveKoWhenLastFalse() { assertEquals(”KO", foo(true,false)); } - La branch coverage è ora 4/4 - Cxty non è elementi mancanti: il numero minimo di percorsi di esecuzione (linearmente indipendenti) è stato coperto 6
Limiti della code coverage • Avere una code coverage elevata è una condizione necessaria ma in certi casi può non essere sufficiente • Il fatto che abbiamo ottenuto il 100% della copertura del codice non significa che abbiamo coperto tutti i possibili comportamenti dell’applicazione • Cicli, ricorsione, ordine di esecuzione dei metodi, codice mancante… • La copertura ci indica solo che abbiamo eseguito il codice durante i test, non che ne abbiamo testato il comportamento in maniera significativa • Il caso limite è quello di test senza asserzioni • Anche in presenza di asserzioni, queste devono essere scelte in modo tale che siano in grado di individuare la presenza di possibili comportamenti anomali • Nella maggior parte dei casi, il problema non risiede nei test presenti, ma nei test mancanti: è necessario considerare dei test case aggiuntivi 7
Mancanza test ai bordi public static String foo(int i) { if ( i >= 0 ) { return "foo"; } else { return "bar"; } } @Test public void shouldReturnFooWhenGiven1() { assertEquals("foo", foo(1)); } @Test public void shouldReturnBarWhenGivenMinus1() { assertEquals("bar", foo(-1)); } • La statement coverage è al 100% • La branch coverage è al 100% • Le asserzioni presenti sono corrette e significative • Ma il comportamento quando (i == 0) non viene verificato 8
Effetti collaterali non testati public static String foo(boolean b) { if ( b ) { importantFunction(); return "OK"; } return "FAIL"; } @Test public void shouldFailWhenGivenFalse() { assertEquals("FAIL", foo(false)); } @Test public void shouldBeOkWhenGivenTrue() { assertEquals("OK", foo(true)); } • La statement coverage è al 100% • La branch coverage è al 100% • Le asserzioni presenti sono corrette e significative • Ma non viene verificato esplicitamente che importantFunction() venga chiamata • Spesso si traduce nel verificare l’avvenuto aggiornamento di uno stato interno 9
Testing usando i mock public static String foo(Collaborator c, boolean b) { if ( b ) { return c.performAction(); } return "FOO"; } @Test public void shouldPerformActionWhenGivenTrue() { foo(mockCollaborator,true); verify(mockCollaborator).performAction(); } @Test public void shouldNotPerformActionWhenGivenFalse() { foo(mockCollaborator,false); verify(never(),mockCollaborator).performAction(); } • La statement coverage è al 100% • La branch coverage è al 100% • Viene correttamente verificato il comportamento nelle interazioni con il mock • Ma manca il controllo sul valore di ritorno • È fondamentale decidere di controllare il valore di ritorno del metodo testato (tramite asserzioni) e/o di verificare le sue interazioni con i mock (tramite verify) 10
Mutation Testing • Il mutation testing è una tecnica per misurare l’efficacia di una test suite • Consiste nel generare in automatico delle versioni modificate dell’applicazione da testare (dette in gergo “mutanti”) inserendo in ognuna un singolo difetto artificiale • Un difetto consiste in una piccola modifica al codice sorgente, a cui dovrebbe corrispondere un comportamento anomalo individuabile tramite i test • Se la test suite è in grado di rilevare il difetto (almeno uno dei test da fail come esito) il mutante è stato eliminato (killed in gergo) • Se la test suite non è in grado di rilevare il difetto (tutti i test danno pass come esito) il mutante è sopravvissuto • Se un mutante è sopravvissuto, uno sviluppatore/tester può ragionevolmente dedurre che i test non stiano controllando effettivamente tutti i possibili comportamenti dell’applicazione • L’efficacia di una test suite è quindi direttamente proporzionale alla percentuale di mutanti uccisi • Nota: il mutation testing viene eseguito solo su di una green test suite, ovvero una test suite in cui tutti i test vengono superati sulla versione originale del codice 11
Esempio di mutazione public static String foo(int i) { public static String foo(int i) { if ( i >= 0 ) { if ( i > 0 ) { return "foo"; return "foo"; } else { } else { return "bar"; return "bar"; CONDITIONALS_BOUNDARY_MUTATOR } } } } originale mutante @Test public void shouldReturnFooWhenGiven1() { assertEquals("foo", foo(1)); } @Test public void shouldReturnBarWhenGivenMinus1() { assertEquals("bar", foo(-1)); } test case eseguiti sul mutante • Nessuno dei due test fallirà, non riuscendo quindi ad uccidere il mutante rilevando il cambiamento nel comportamento causato dalla modifica inserita • Questo suggerisce l’aggiunta del test case mancante @Test public void shouldReturnFooWhenGiven0() { assertEquals("foo", foo(0)); } 12 test case da aggiungere
Limite: mutazioni equivalenti public static String foo(int i) { public static String foo(int i) { int i = 2; int i = 2; if ( i >= 1 ) { if ( i > 1 ) { return "foo"; return "foo"; } } CONDITIONALS_BOUNDARY_MUTATOR return ”bar"; return ”bar"; } } originale mutante • In questo caso la modifica ha creato un mutante con un comportamento equivalente a quello originale • Il mutante non potrà quindi essere ucciso in alcun modo • Un esempio comune è una mutazione applicata alle istruzioni di logging o di debug: in quel caso è il programmatore a non essere interessato al testing di questi comportamenti • Dal report dell’analisi o dalla configurazione del tool di mutation testing è possibile in molti casi individuare o escludere queste casistiche 13
PITest • Tool di mutation testing per Java e JVM • Eseguito da riga di comando o come plugin per Maven, Gradle e Ant • Dispone di diversi operatori di mutazione (mutatori) • Le mutazioni vengono applicate a livello di bytecode • Facile da configurare tramite Maven • Report che integra copertura del codice e copertura dei mutanti • Codice open source: https://github.com/hcoles/pitest • Estendibile con nuovi operatori o nuovi engine • Supporto all’extreme mutation testing 14
Esempio Report di PIT @Test public void shouldReturnFooWhenGiven1() { assertEquals("foo", foo(1)); } @Test public void shouldReturnBarWhenGivenMinus1() { assertEquals("bar", foo(-1)); } test case presenti 100% di code (line) coverage ma 75% di mutation coverage Il report mostra che 1 dei 2 mutanti in riga 6 è sopravvissuto: Mutatore: CONDITIONALS_BOUNDARY_MUTATOR Dalla documentazione, il mutatore: - converte >= in > e viceversa - converte
Aggiunta di un nuovo test case @Test public void shouldReturnFooWhenGiven1() { assertEquals("foo", foo(1)); } @Test public void shouldReturnBarWhenGivenMinus1() { assertEquals("bar", foo(-1)); } @Test public void shouldReturnFooWhenGiven0() { assertEquals(”foo", foo(0)); } test case aggiornati Ora sia code (line) che mutation coverage sono al 100% 16
Esecuzione con Maven Si aggiunge il plugin al build lifecycle org.pitest Per eseguire la mutation coverage dobbiamo pitest-maven 1.5.2 prima aver i test compilati (mvn test) mvn pitest:mutationCoverage org.isd.BoundExample (Opzionale) è possibile aggiungere dei org.isd.TestBoundExample parametri di configurazione, ad esempio: È possibile limitare la generazione dei mutanti ad alcune classi (targetClasses) CONSTRUCTOR_CALLS NON_VOID_METHOD_CALLS È possibile indicare quali classi usare per la mutation coverage (targetTests) È possibile indicare quali mutatori, o gruppi di mutatori, usare (mutators) 17
Operatori di mutazione di default Nota: ogni mutante avrà sempre una sola modifica Mutatori Descrizione Conditionals Boundary = ßà > Increments i++ ßà i-- Invert Negatives -x à x Math es. a+b ßà a-b; a*b ßà a/b Negate Conditionals == ßà != >= ßà < Void Method Calls elimina una chiama ad un metodo void Empty returns ristituisce un valore “vuoto“ in base al tipo di ritorno (es. Stringa vuota, Lista vuota, zero) False Returns come il precedente, ma per il tipo boolean: restituisce false True returns come il precedente, ma restituisce true Null returns restituisce null PIT supporta diversi altri mutatori più “forti”, selezionabili in fase di configurazione es. Non Void Method Call Mutator: rimuove una chiamata ad un metodo non void, sostituendo il valore di ritorno con il valore di default per il tipo (null, false, 0, 0.0, ‘\u0000’) es. Remove Increments Mutator: rimuove un’operazione di incremento di una variabile 18
Funzionamento di PIT • Per ogni classe dell’applicazione, PIT visita ogni suo metodo cercando istruzioni a cui applicare i mutatori selezionati (visitor pattern) • Il numero di mutanti generati dipende quindi sia dal numero di mutatori che dalla dimensione dell’applicazione (numero di istruzioni) • Per ciascuno dei mutanti generati verrà effettuata l’esecuzione dei test, in modo da verificare se il mutante viene ucciso o meno • Questo comporta l’esecuzione di un gran numero di test: il mutation testing è molto oneroso rispetto alla misura di copertura del codice • Per limitare il numero di test da eseguire per ciascun mutante, prima di generare i mutanti PIT esegue un’analisi della copertura del codice • Per ogni test viene tenuta traccia delle istruzioni coperte • Preso quindi un mutante, verranno selezionati solo quei in grado di coprire le istruzioni modificate dall’operatore di mutazione 19
Extreme mutation testing • Dato un metodo da mutare, invece di modificarne una singola istruzione l’intero corpo del metodo viene eliminato e sostituito da una sola istruzione di return • Utile per individuare rapidamente dei metodi pseudo-testati, ovvero metodi coperti dalla test suite (chiamati almeno una volta) ma la cui modifica, per quanto estrema, non viene comunque individuata • Mutazione molto più rapida ed “economica”: il numero di mutanti dipende solo dal numero di metodi, invece che dal numero di istruzioni e di mutatori • Da usare come fase preliminare, prima di applicare gli operatori di mutazione tradizionali, che genererebbero molti più mutanti richiedendo l’esecuzione di più test 20
Descartes Mutation Engine • Si basa su PIT e ne sostituisce il mutation engine di default (Gregor) • Il mutation engine si occupa della creazione dei mutanti tramite i mutatori • Applica gli operatori di mutazione estremi • Individuazione rapida e meno costosa dei metodi pseudo-testati Descartes Mutation Engine (extreme m. t.) @Test public void shouldReturn1WhenGiven0() { assertEquals(1, factorial(0)); } @Test public void shouldReturn1WhenGiven1() { assertEquals(1, foo(1)); } Test Suite da valutare Si può notare come già il solo mutante estremo sia sufficiente a dimostrare l’inadeguatezza della test suite (nessuno dei due test fallisce, riuscendo così ad uccidere il mutante); Gregor Mutation Engine (PIT default) suggerisce quindi aggiungere almeno un altro caso di test, es. assertEquals(120, fact(5)); 21
Regression Testing • Attività di testing che viene eseguita per accertarsi che l’introduzione di nuove modifiche non abbia compromesso la correttezza delle funzionalità esistenti • Questo permette di individuare tempestivamente eventuali difetti inavvertitamente introdotti nel codice anche in parti non direttamente oggetto delle nuove modifiche • Es. l’aggiornamento del formato di output di una funzionalità di libreria ha causato un errore di parsing in un componente che la richiama • Essendo un’operazione frequente, tipicamente ci si avvale di strumenti automatici per l’esecuzione dei test di regressione (JUnit, CI pipelines…) • Il modo più semplice consiste nel rieseguire tutti i test (retest all), ma al crescere dell’applicazione il numero di test presenti aumenta (es. il numero di unit test è proporzionale al numero di metodi) • Per rendere il regression testing cost-effective, vari approcci sono stati proposti: • test suite minimisation: identificare ed ignorare i test case ridondanti e obsoleti • test suite reduction: identificare ed eliminare i test case ridondanti e obsoleti • test case selection: selezionare i test che esercitano il codice influenzato dalle modifiche introdotte (sia direttamente che indirettamente) • test case prioritisation: ordinare i test secondo un criterio scelto (es. copertura) 22
Test Suite Minimisation & Reduction • Entrambi puntano ad individuare test case ridondanti o obsoleti con la differenza che: • per minimisation questi test case vengono solo ignorati (es. per avere una test suite più efficiente da eseguire spesso, posticipando un retest all a dopo le 18:00) • per reduction questi test case vengono eliminati, riducendo permanentemente la dimensione della test suite • Tipicamente viene risolto come un problema di ottimizzazione: • scelto un criterio di copertura (ed. del codice o dei mutanti) • viene individuato un sottoinsieme di test case in grado di mantenere la stessa copertura • con un minor numero di test o un minor tempo di esecuzione • Gli approcci si distinguono inoltre in: • adeguati: non vi è perdita di copertura nella test suite risultante • non adeguati: accettata una percentuale di perdita di copertua (rilassamento) 23
Approcci di Minimisation & Reduction • L’approccio più comunemente usato è quello greedy: • il criterio di copertura determina l’insieme di elementi da coprire (istruzioni, branch, mutanti…) • ad ogni passo viene selezionato un test case in grado di coprire il maggior numero possibile di elementi non ancora coperti (elementi: istruzioni, branch, mutanti…) • la selezione di nuovi test continua fino a che tutti gli elementi non vengono coperti • Altri approcci si basano sulla risoluzione di un problema di set cover: • data una famiglia di insiemi, l’obiettivo è trovare la sottofamiglia di dimensioni minime in grado di coprire tutti gli elementi della famiglia di input • il criterio di copertura determina l’insieme di elementi della famiglia input (istruzioni, branch, mutanti… da coprire) • ogni test case viene visto come un insieme di elementi (quelli da lui coperti) • Il problema può essere risolto con un approccio di Programmazione Lineare Intera • Funzione obiettivo da minimizzare: #test o tempo di esecuzione S3 è ridondante • Vincoli: ogni elemento deve essere coperto da almeno un test 24 M. Mongiovì, A. Fornaia, and E. Tramontana. "REDUNET: reducing test suites by integrating set cover and network-based optimization." Applied Network Science 5.1 (2020): 1-21.
Test Suite Selection • L’obiettivo è selezionare solo quei test relativi a parti di codice direttamente o indirettamente coinvolte dalle ultime modifiche introdotte nel codice • Questo per individuare rapidamente difetti introdotti con le ultime modifiche, correggendoli in maniera tempestiva • È immediato individuare i test che chiamano direttamente il codice modificato (es. il metodo o la classe in cui sono state inserite le modifiche) • Meno immediato se l’interazione è indiretta (il test chiama un metodo che chiama un metodo…) • La soluzione naturale è: • dopo aver apportato delle modifiche eseguire tutti i test una volta sola, per poi rieseguire solo i test falliti, finché questi non vengono risolti • Ma il problema è proprio evitare di rieseguire tutti i test, anche se una sola volta • La soluzione intuitiva è usare le informazioni sulla copertura e rieseguire solo i test che coprono il codice modificato: • Male per ottenere le informazioni di copertura aggiornate dovrei rieseguire tutti i test sulla nuova versione del codice • Si può limitare usando l’analisi statica delle dipendenze e l’esecuzione simbolica per determinare quali test potrebbero “potenzialmente” coprire il codice modificato 25
Test Suite Prioritisation • Mira ad individuare l’ordinamento ideale dei test case, in modo da ottenere il massimo dei benefici anche se l’esecuzione dei test viene interrotta prematuramente • L’obiettivo è quello di massimizzare la probabilità di individuare dei difetti eseguendo solo una (prima) parte dei test a disposizione (maximize early fault detection) • Alcuni approcci usano un ordinamento basato su un criterio di copertura (es. eseguo prima i test in grado di uccidere più mutanti) • Altri approcci usano informazioni sulle statistiche dei difetti rilevati da ciascun test • Altri danno priorità ai test falliti di recente, perché hanno una maggior probabilità di fallire ancora 26
Generazione Automatica di Test • La creazione di test da parte dello sviluppatore è un’operazione necessaria ma faticosa e in alcuni casi ripetitiva • Raggiungere un buon livello di copertura può richiede la creazione di un numero considerevole di test case • Spesso, certe parti del test sono ripetute (boilerplate) e trovare il giusto modo per riusare test (o porzioni) precedentemente scritti può aiutare la creazione di nuove test • In letteratura diversi approcci e tool sono stati proposti per la generazione automatica di test case • Nella lezione sul Testing Combinatoriale abbiamo visto Coffee4j per la generazione combinatoriale di test case • Qui vedremo altri 3 approcci differenti: • Generazione randomica • Generazione evolutiva • Generazione basata su template 27
Randoop • Genera automaticamente test JUnit completi di asserzioni • I test vengono generati usando un approccio random con feedback. • Data un insieme di classi da testare: • genera test come sequenze casuali di chiamate ai metodi di queste classi • esegue le sequenze generate, utilizzando il risultato (o l’eccezione) ottenuto per creare delle asserzioni • I test ottenuti, completi di asserzioni, descrivono il comportamento attuale dell’applicazione (giusto o sbagliato che sia…) • Pensato per generare test per un intero progetto (--testjar) non per la singola classe • Alternativamente, si può pensare di limitare la generazione alla classe da testare e a tutte le classi da cui questa dipende (da inserire in un file di testo e da passare con --classlist) • Se genero i test di una classe senza passare anche le classi da cui dipende probabilmente non riuscirà a generare molti test (perché non potrà usarle all’interno del codice dei test) • È open source: https://github.com/randoop/randoop • Disponibile come tool da riga di comando 28
Esempio test generati da Randoop Regression test: Generazione di test (come sequenze anche lunghe di chiamate ai metodi delle classi selezionate) che descrivano il comportamento del codice attuale: - in termini di valori restituiti - eccezioni scatenate I test vengono completati con le asserzioni in base al valore (o all’eccezione) ottenuta dall’esecuzione del test senza asserzione Dopo aver modificato il codice, questi test possono essere rieseguiti per vedere se queste modifiche hanno alterato involontariamente il comportamento di altre classi Error revealing test: sono test che riescono a dimostrare (dando esito fail) la violazione di un contratto: - riflessività di equals: se un metodo testato restituisce un oggetto o allora o.equals(o) == true (vedi esempio a sinistra) - simmetria di equals: o1.equals(o2) == o2.equals(o1) - e altri su hashCode(), clone(), toString(), compareTo() di Object - ulteriori contratti possono essere definiti del programmatore Randoop cerca di generare delle sequenze (anche piuttosto lunghe) in grado di violare questi contratti Un contratto violato, è indice di un errore da correggere nel 29 codice
EvoSuite • Genera automaticamente test JUnit completi di asserzioni (https://github.com/EvoSuite/evosuite) • Disponibile sia come jar stand alone, che come plugin per Eclipse o per Jenkins • Evosuite genere test per ciascuna classe: ognuna sarà di volta in volta la Class Under Test (CUT) • I test vengono generati usando un approccio evolutivo tramite algoritmo genetico • Data una o più classi da testare: • usa un approccio randomico per generare una popolazione di test suite (nota, non test case, intere test suite!) • usando un criterio di copertura come fitness function (es. branch coverage raggiunta da ciascuna test suite) vengono selezionati i migliori individui per procreare la prossima generazione • come operatore di crossing tra due individui (test suite) viene usato lo scambio casuale di test case • come operatore di mutazione di un individuo, vengono aggiunti, eliminati o modificati i test case presenti nella test suite (modifica di un test case: aggiunta, eliminazione o modifica di istruzioni/parametri) • Il processo continua in maniera iterativa finché non si arriva a convergenza o si raggiunge un limite massimo di iterazioni/tempo, selezionando la test suite che massimizza la copertura • La test suite selezionata è oggetto di ulteriori passi di test suite reduction per eliminare i test case e le istruzioni che non contribuiscono al miglioramento della copertura 30
Templating • Usare test già presenti come template per creare nuovi test per classi simili nella stessa applicazione • Per ogni test viene individuato l’insieme di classi testate (chiamata ad almeno uno dei suoi metodi) dette classi target • Per ogni classe target vengono individuate altre classi nel sistema con un comportamento simile, determinato in basse alle API utilizzate • Si genera un nuovo test a partire da quello presente (template), sostituendo le chiamate ai metodi della classe testata con chiamate ai metodi della classe simile • Vengono usate varie regole per determinare come e con quale metodo sostituire le chiamate esistenti (es. metodi con nome o parametri molto simili, se non uguali) • Tutte queste modifiche avvengono a livello di codice sorgente usando JavaParser • Questo permette di aumentare la copertura, aggiungendo test per le classi già esistenti o per classi precedentemente non coperte A. Fornaia, A. Midolo, G. Pappalardo, E. Tramontana: “Automatic Generation of Effective Unit Tests based on Code Behaviour.” Proceedings of the 29th 31 IEEE International Conference on Enabling Technologies: Infrastructure for Collaborative Enterprises (WETICE), 2020.
Dataset di bug reali: Defects4j • È un database di difetti reali (bug) provenienti da varie versioni di progetti Java open- source famosi ed usati (https://github.com/rjust/defects4j) • Viene fornito con opportuni automatismi per facilitarne l’uso come sistema di benchmarking per i tool di generazione automatica di test • Ad oggi (Novembre 2020) contiene 835 bug provenienti da 17 progetti open-source • Per ogni bug: • viene catalogata una versione del codice con il bug (buggy) e una versione del codice senza il bug (fixed), isolando il bugfix da altri tipi di modifiche (feature, refactoring) • Viene fornita la lista di classi coinvolte dal bugfix • Viene inoltre fornito un test in grado di mostrare la presenza del bug (passa sulla versione fixed ma fallisce sulla versione buggy) 32
Benchmarking con Defects4j • Defects4j viene usato per valutare l’efficacia del proprio tool di generare test in grado di individuare bug reali • Dato un bug, è possibile usare il proprio tool (o Randoop, EvoSuite) per generare i test per le classi coinvolte dal bug • Tipicamente i test vengono generati sulla versione fixed per poi vedere se almeno uno di questi fallisce quando eseguito sulla versione buggy (simulando, a ritroso, l’introduzione del bug) Esempio di patch disponibile relativa ad un bugfix in Jsoup (parser HTML) In rosso (controintuitivamente…) il codice di Vfix In verde (controintuitivamente…) il codice di Vbug 33
Defects4J Dissection • http://program-repair.org/defects4j-dissection • https://github.com/program-repair/defects4j-dissection • Permette di esplorare i bug presenti in Defects4J in maniera interattiva tramite interfaccia web • Per ogni bug viene mostrata: • la relativa patch (diff) • una classificazione delle repair action coinvolte (es. aggiunto ramo if, mod. espressione logica…) • una classificatoine dei repair pattern coinvolti (modifica condizione if…) • le eccezione associata al bug (es. AssertionFailedError, NullPointerException…) 34
Esempio Bug 35
Riferimenti • J. B. Rainsberger: “JUnit Recipes. Practical Methods for Programmer Testing” • T. Kaczanowski: “Practical Unit Testing with Junit and Mockito” • PITest, documentazione: https://pitest.org • JaCoCo Coverage: https://www.eclemma.org/jacoco/trunk/doc/counters.html • H. Coles et al. “Pit: a practical mutation testing tool for java.” International Symposium on Software Testing and Analysis. 2016. • O.L. Vera-Pérez et al. “Descartes: a PITest engine to detect pseudo-tested methods: tool demonstration.” International Conference on Automated Software Engineering (ASE). IEEE, 2018. • R. Just et al. "Defects4J: A database of existing faults to enable controlled testing studies for Java programs." International Symposium on Software Testing and Analysis. 2014. • C. Pacheco, and M. D. Ernst. "Randoop: feedback-directed random testing for Java." ACM SIGPLAN conference on Object-oriented programming systems and applications companion. 2007. • G. Fraser, and A. Arcuri. "Evosuite: automatic test suite generation for object-oriented software." Proceedings of the 19th ACM SIGSOFT symposium and the 13th European conference on Foundations of software engineering. 2011. • S. Yoo, and M. Harman. "Regression testing minimization, selection and prioritization: a survey." Software testing, verification and reliability 22.2 (2012): 67-120. • M. Mongiovì, A. Fornaia, and E. Tramontana. "REDUNET: reducing test suites by integrating set cover and network- based optimization." Applied Network Science 5.1 (2020): 1-21. • A. Fornaia, A. Midolo, G. Pappalardo, E. Tramontana: “Automatic Generation of Effective Unit Tests based on Code Behaviour.” Proceedings of the 29th IEEE International Conference on Enabling Technologies: Infrastructure for Collaborative Enterprises (WETICE), 2020. • V. Sobreira et al. "Dissection of a bug dataset: Anatomy of 395 patches from defects4j." 2018 IEEE 25th 36 International Conference on Software Analysis, Evolution and Reengineering (SANER). IEEE, 2018.
Puoi anche leggere