Che cos’è, come funziona e Code Coverage nel TDD (Test-Driven Development)

Che cos’è, come funziona e Code Coverage nel TDD (Test-Driven Development)

Test-Driven Development

Il Test-Driven Development (abbraviato con TDD), come dice il nome, è lo sviluppo di codice guidato dai test, questo modello è basato su delle semplici regole che andiamo a dettagliare di seguito:

  • Non scrivere codice in produzione prima di aver uno unit test che fallisce;
  • Non scrivere più codice di test di quello necessario ad ottenere un fallimento, considerando l’impossibilità di compilare un fallimento;
  • Non scrivere più codice di produzione di quello necessario a far passare i test case che falliscono.

In questo modo il codice di production e quello di testing vanno di pari passo aumentando la coverage ovvero la percentuale di codice attraversato dai test rispetto al totale del codice.
Il processo di scrittura di codice diviene quindi il cosiddetto “Red-Green-Refactor cycle”, in quest’ultimo, partendo da un test che fallisce (Red), si scrive il codice che lo fa passare (Green) per poi sistemare quest’ultimo (Refactor); il tutto normalmente dovrebbe avvenire in 10-20 minuti.

Che cos'è, come funziona e Code Coverage nel TDD (Test-Driven Development)

Il TDD, quindi, incoraggia il refactoring e la modifica del codice, infatti, avendo delle suite di test pronte, gli sviluppatori non avranno paura ad effettuare i cambiamenti necessari ad aumentare flessibilità, manutenibilità e riusabilità del codice, potendosi immediatamente accorgere di eventuali errori. L’utilizzo massivo dei test significa anche che il codice degli stessi possa diventare molto voluminoso, e quindi difficile da mantenere, per evitare problemi è necessario tenerlo più pulito possibile, considerandolo allo stesso livello
del production code.

Avere dei test scarsamente mantenibili pùo essere considerato peggiore di non avere nessun test, infatti, gli stessi dovranno cambiare continuativamente lungo la vita del progetto, e uno sforzo troppo alto nella modifica potrebbe portare gli sviluppatori a:

  • Non eseguire i test;
  • Non considerare i fallimenti dei test;
  • Perdere moltissimo tempo per sistemare i test. Rendendo di fatto la scrittura dei test stessi inutile.

Per mantenere le suite di testing pulite la cosa più importante è la leggibilità, un buon test deve essere leggibile facilmente e in modo rapido, senza confondere lo sviluppatore sullo scopo dello stesso. Non è sempre semplice promuovere la readability; un approccio spesso usato è costruire delle API di alto livello che vengano utilizzate dai test, in modo da nascondere molta della complessità sottostante, non necessaria alla comprensione. In questo modo si vengono a creare dei Domain Specific Languages (DSLs) utilizzati soltanto per la scrittura dei test.

Alcuni linguaggi moderni, come Scala e Kotlin, hanno caratteristiche che permettono di aumentare al massimo la leggibilità delle suite, tra le quali la possibilità di utilizzare linguaggio naturale nel nome del test (ad esempio “create leader and member, add task and change task status”) o il supporto alla scrittura di DSLs.

Oltre alla leggibilità i test dovrebbero avere le cosiddette caratteristiche FIRST:

  1. Fast
  2. Indipendent
  3. Repeatable
  4. Self-Validating
  5. Timely

I test devono quindi essere, veloci, indipendenti tra loro, ripetibili, autovalidanti e scrivibili velocemente.

La velocità di un test è fondamentale, infatti, se un test è lento, gli sviluppatori saranno meno portati a lanciarlo e quindi sarà più difficile scovare eventuali errori.

I test devono essere indipendenti l’uno dall’altro, in modo da poter essere lanciati separatamente e in qualsiasi ordine. Se si hanno troppe dipendenze è possibile che si generino fallimenti a catena, che rendono difficile l’individuazione dei problemi.

Il determinismo dei test in ogni ambiente è fondamentale, in mancanza di questa caratteristica potranno generarsi falsi positivi e falsi negativi, che pian piano spingeranno i programmatori a non eseguire i test.

Un test ovviamente deve dire in automatico se ha ottenuto il risultato sperato o no, nessuna azione (ad esempio lettura di log) deve essere necessaria da parte del programmatore per ottenere il risultato.

Anche la velocità di scrittura delle prove è molto importante, infatti, se un test richiede troppo tempo per essere scritto, è molto probabile che non venga scritto o che venga implementato male, abbassando la coverage dell’intero progetto.

I principi del TDD indicati in questa sezione si riferiscono normalmente agli Unit Test, cioè i test sviluppati per una singola funzionalità.

Code Coverage

La code coverage viene solitamente definita come la percentuale di codice attraversato dei test rispetto al totale della code base, tuttavia non esiste una definizione precisa, infatti bisogna specificare esattamente cosa significa “coprire” una parte di codice e a che livello (modulo, classe, metodo etc.).
La coverage normalmente viene utilizzata per identificare parti non testate dell’applicazione, tuttavia, citando Dijkstra ricordiamo che “Il testing mostra la presenza di errori, non la loro assenza”, per cui, anche un codice con 100% di coverage, non sarà immune da bugs. Lo scopo non è quindi aumentare al massimo la coverage, ma testare le parti appropriate dell’applicazione.

Test doubles

I test doubles sono oggetti di test che sostituiscono le implementazioni reali, nel TDD sono fondamentali per riuscire a sviluppare test velocemente e continuativamente. Per esempio, per testare una classe A che contiene un oggetto della classe B ci sono due modalità:

  • Creare il test per la classe A e successivamente creare la classe A e la classe B;
  • Creare il test per la classe A e successivamente creare la classe A utilizzando un test double per la classe B.

Il secondo approccio è migliore soprattutto quando l’implementazione della classe B risulterebbe particolarmente lunga. Un altro vantaggio sta nella possibilità di non scrivere il codice relativo a B prima che venga scritto il test di B, rimanendo quindi fedeli ai principi del TDD.

Esistono vari tipi di test doubles:

  • Dummy, oggetti passati, ma che non vengono mai utilizzati, solitamente servono a riempire liste di parametri;
  • Stubs, implementazioni semplici, con metodi che non svolgono alcuna funzione e ritornano valori di default;
  • Fake Objects, oggetti funzionanti, ma che utilizzano varianti semplici (algoritmi meno efficienti, DB in RAM);
  • Test Spies, oggetti “spia” simili agli stub, ma che loggano qualsiasi chiamata venga fatta loro;
  • Mocks, come i test spies, ma che hanno comportamenti particolari (e diversi dall’implementazione reale) sotto certe circostanze (ad esempio “se la data è x usa l’algoritmo y e inserisci z nel log”).

I test doubles hanno altre funzioni oltre a quella di velocizzare il TDD, infatti servono anche per rendere deterministici dei comportamenti randomici, per isolare il codice dalle proprie dipendenze, per osservare proprietà altrimenti invisibili e per simulare particolari condizioni difficili da ottenere normalmente. Il tutto per riuscire ad individuare in modo migliore gli errori.

Pubblicato da Vito Lavecchia

Lavecchia Vito Ingegnere Informatico (Politecnico di Bari) Email: [email protected] Sito Web: https://vitolavecchia.altervista.org

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *