I principi base di una buona suite di test sono condivisi tra tutti:
- efficaci
- veloci
- manutenibili ..
Entrando in maggiore dettaglio iniziano numerose guerre di religione e filosofie differenti e non sempre conciliabili.
Code Coverage
Nel ricordare la peggiore esperienza nella scrittura di test mi viene in mente un cliente che richiedeva a tutti i team di sviluppo di raggiungere una code coverage non inferiore al 90 % .
In quel momento i vari team avevano una copertura di circa il 20 %, ed hanno interrotto gli sviluppi per aderire alla nuova regola.
Mi ricordo che, trovando assurda quella scelta, scrissi un test che via reflection andava a verificare tutti i getter / setter in automatico, aiutando i team a raggiungere l'obiettivo. Chiaramente era una provocazione sottile, ma formalmente corretta.
Che conseguenza ha portato l'introduzione di quella regola ? Le logiche di programmazione si erano spostate dal codice java ai sistemi dove lo strumento di code coverage non aveva 'potere': il database con stored procedures complesse e le view jsp.
Dire quindi che è importante avere 'tanti' test non ha chissà che valore, rispetto ad averne anche pochi ma assai efficaci.
Ma quale principio ci guida nella scrittura dei test?
Il peggiore che abbia mai incontrati è
Il principio delle scimmia
piu volte enunciato da vari colleghi incontrati nella storia: se di notte una scimmia cancella una riga a caso della tua code base , almeno un test si deve rompere.
E' un principio sensato, se non fosse che: no, non ci sono scimmie di quel genere! E' anche vero che un qualsiasi programmatore può cancellare una riga per sbaglio...ma non ha senso basare la tua strategia di scrittura di test su una paura irrazionale e statisticamente improbabile.
Un altro principio che reputo errato è
Gli acceptance test devono essere end-to-end
Tale principio è arrivato tardi nella mia esperienza lavorativa. I primi AT (Acceptance Test) li scrissi con Fit/Fitnesse. Avevano forma tabellare o erano narrativi, e interrogavano le logiche di business implementate nel dominio. L'input, in qualsiasi forma arrivasse, tramite chiamate http , o file di testo, o comandi in una shell, non era importante. Era importante la logica sottostante, il motore del tuo progetto.
Dopo alcuni anni ho notato questa uguaglianza logica nella testa di molti: un AT deve esercitare i sistemi "veri". Ad esempio se è una webapp devono girare in un browser.
Secondo me i test end to end non sono acceptance test nella loro idea originaria.
Al di là della nomenclatura, anche i test di 'integrazione' con i sistemi esterni che vedo scritti in giro non vanno bene!
I test di integrazione devono garantire che il tuo sistema sa interpretare correttamente i dati di input e che costruisce corretti dati di output, in totale aderenza ad un protocollo comunicativo.
Ad esempio durante lo sviluppo di un ecommerce che si basa su paypal, non ha senso che i tuoi test passino dalla sandbox simulando il giro completo effettuato da un utente.
In tal modo ti stai legando al funzionamento di un sistema che non è sotto il tuo controllo e i tuoi test stanno testando il comportamento di un altro sistema su cui non stai lavorando !
E' invece importante garantire che le chiamate http scambiate tra il tuo ecommerce e paypal siano corrette ! (ma senza necessità di una simulazione via browser)
Riassumendo:
- gli AT devono esplicitare la logica applicativa. Non è necessario che siano di integrazione
- i test di integrazione devono testare solo il protocollo di comunicazione (input e output) e basta !
Buoni principi
Per metterti in condizione di poter testare il tuo codice le dipendenze vanno iniettate. Non sono particolarmente a favore di librerie di dependency injection (non ti danno nulla in piu di farlo tu direttamente) ma non è questo il punto. E' importante non avere dipendenze dirette nel codice !
Portando questo principio su diversi livelli, dal singolo oggetto al modulo applicativo, riuscirai a trasformare il tuo sistema in una 'stanza dei bottoni' dove, a piacere, puoi switchare tra un database vero e uno in memoria, tra un sistema di messaggistica via code vero a uno finto, tra una dipendenza vera a una finta.
Questo tipo di flessibilità l'ho visto applicato solo in poche occasioni in team molto maturi. Ma quando ho avuto modo di lavorare su un progetto dominato dalla 'stanza dei bottoni' il divertimento e la produttività sono stati a livelli superlativi !
In quasi tutti i progetti che ho lavorato ho sempre spinto verso l'adozione di 'builder' : oggetti che ti costruiscono un oggetto di dominio di 'default' , con tutte le dipendenze necessarie e che va bene nella maggior parte dei casi.
Ad esempio in un progetto di ecommerce, è utile avere un 'productBuilder' che , a fronte della chiamata di un metodo 'build' ti dà un prodotto generico , disponibile in magazzino, con un prezzo valido, una descrizione normale etc.
La maggior parte dei test lavora su piccole variazioni in merito, ed è pertanto importante costruire una struttura che ti dia facilmente il default ma che ti permetta anche di iniettare piccole modifiche. Il product builder potrebbe essere customizzato cosi:
new productbuilder.build()
new productbuilder.withName('something').build()
new productbuilder.withPrice(12).build()
new productbuilder.withQuantity(5).build() ...
Una tipologia di test difficili da scrivere e ideare, ma assai utili, sono quelli relativi ad uno 'scenario' . I builder rappresentano in vitro ciò che si può fare ad un più elevato livello di astrazione: la costruzione di uno scenario 'tipo' e la possibilità di introdurre flessibilità al fine di verificare il comportamento del tuo sistema.
Voglio rimanere volutamente generico, ma quando sono riuscito a creare dei test di scenario con i miei compagni di team ne ho trovato davvero molto giovamento!