Mutation & Regression Testing - Andrea Fornaia, Ph.D. Department of Mathematics and Computer Science - DMI Unict

Pagina creata da Letizia Baroni
 
CONTINUA A LEGGERE
Mutation & Regression Testing - Andrea Fornaia, Ph.D. Department of Mathematics and Computer Science - DMI Unict
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
Mutation & Regression Testing - Andrea Fornaia, Ph.D. Department of Mathematics and Computer Science - DMI Unict
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
Mutation & Regression Testing - Andrea Fornaia, Ph.D. Department of Mathematics and Computer Science - DMI Unict
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
Mutation & Regression Testing - Andrea Fornaia, Ph.D. Department of Mathematics and Computer Science - DMI Unict
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
Mutation & Regression Testing - Andrea Fornaia, Ph.D. Department of Mathematics and Computer Science - DMI Unict
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
Mutation & Regression Testing - Andrea Fornaia, Ph.D. Department of Mathematics and Computer Science - DMI Unict
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
Mutation & Regression Testing - Andrea Fornaia, Ph.D. Department of Mathematics and Computer Science - DMI Unict
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
Mutation & Regression Testing - Andrea Fornaia, Ph.D. Department of Mathematics and Computer Science - DMI Unict
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