Un test per place_order ha bisogno di un gateway di pagamento, un client SMS e un calcolatore delle tasse. Se collegassi quelli veri, ogni esecuzione del test addebiterebbe una carta e manderebbe un SMS a un numero reale. Un test unitario deve girare in pochi millisecondi, senza rete, quindi tutti e tre vanno sostituiti con qualcos’altro. Quel “qualcos’altro” si presenta in cinque forme — ognuna risolve un problema diverso — e trattarle come intercambiabili è il motivo per cui le discussioni “basta mockarlo” in code review girano in tondo.
Cos’è un test double
Gerard Meszaros ha coniato il termine “test double” in xUnit Test Patterns come termine ombrello per qualunque cosa sostituisca una dipendenza reale durante un test, allo stesso modo in cui una controfigura sostituisce un attore sul set. Le troupe cinematografiche usano controfigure diverse per inquadrature diverse: una deve solo stare sullo sfondo con il taglio di capelli giusto, un’altra fa davvero lo schianto con l’auto. I test double si dividono nello stesso modo. Dummy, stub, spy, fake e mock fanno cinque lavori diversi, e ciò che li separa è cosa verifica il test, non quanto sia elaborato l’oggetto sostitutivo.
Ecco la classe che ogni esempio qui sotto testa. Tre delle sue dipendenze vale la pena sostituirle; la quarta, come vedrai, non conta affatto:
| |
logger entra in gioco solo sul percorso d’errore. Questo conta per il primo esempio.
Due piccoli sostituti vengono riusati nei prossimi test, quindi li definiamo una volta sola qui:
| |
Cos’è un oggetto dummy nei test
Un dummy viene passato solo per soddisfare un parametro che il test non esercita mai. Non fa nulla e non verifica nulla. Il test del percorso felice qui sotto non arriva mai al ramo dell’eccezione, quindi logger può essere qualunque cosa sopravviva all’assegnazione a un attributo:
| |
object() funziona perché nessuno chiama un metodo su di esso. Se una modifica successiva facesse loggare place_order anche sul percorso di successo, questo test fallirebbe con un AttributeError. Quel fallimento è utile: ti dice che il dummy ha smesso di essere un dummy.
Cos’è uno stub nei test unitari
Uno stub restituisce una risposta prestabilita per mantenere il resto del test deterministico. Il calcolatore delle tasse vero potrebbe chiamare un’API delle aliquote. Il test non vuole quello, quindi StubTaxCalculator restituisce sempre 108.00, qualunque subtotale riceva. Lo hai già visto all’opera nel test del dummy sopra, insieme a StubGateway che fa lo stesso lavoro per charge.
Niente verifica se total_with_tax è stato chiamato, o con quale argomento. Uno stub fornisce al codice sotto test un input controllato; non verifica mai un’interazione. Questa è la linea che separa uno stub dai tre test double successivi.
Cos’è un test spy
Uno spy registra le chiamate che riceve così il test può ispezionarle dopo che il codice sotto test è stato eseguito. unittest.mock.Mock registra automaticamente ogni chiamata, quindi la maggior parte delle suite Python lo usa come spy anche quando la parola “spy” non compare mai nel test:
| |
Nessuna configurazione è stata impostata su sms_spy prima della chiamata. Ha accettato qualunque cosa gli sia stata data, l’ha registrata, e il test l’ha interrogata dopo.
Cos’è un oggetto fake nei test
Uno stub non può dirti se due addebiti consecutivi ottengono due ID diversi, perché conosce solo un valore. Quando un test ha bisogno di un comportamento attraverso più chiamate, serve un fake: una piccola implementazione che funziona davvero.
| |
| |
FakeGateway non ha chiamate di rete né un vero processore di pagamenti dietro, ma mantiene uno stato come farebbe quello vero. Il test aveva bisogno esattamente di quello stato, e uno stub non poteva fornirlo.
Cos’è un oggetto mock
Nella definizione di Meszaros, un mock porta le sue aspettative dentro il test: le regole per una chiamata valida sono fissate prima che il codice venga eseguito, e una chiamata che le viola fallisce sul colpo. unittest.mock è più permissivo di così, perché verifichi comunque dopo, ma create_autospec ti porta a metà strada verso quel comportamento. Costruisce il double a partire da una classe reale, quindi ogni chiamata viene controllata contro le firme reali dei metodi nel momento in cui avviene:
| |
Modifica place_order per chiamare self.gateway.charge(total, currency="usd") e questo test fallisce con un TypeError dentro place_order, perché Gateway.charge non accetta un argomento currency. Immagina ora un semplice Mock() nello stesso punto, con qualcuno che aggiorna l’asserzione per farla corrispondere alla nuova chiamata: il test passa, e il codice si rompe la prima volta che incontra il gateway reale. Uno spy conferma che una chiamata è avvenuta. Un mock autospecced conferma anche che la chiamata è una di quelle che l’oggetto reale accetterebbe.
Mock vs stub vs spy vs fake: tabella di confronto
| Test double | Cosa fa | Usalo quando |
|---|---|---|
| Dummy | Passato per soddisfare un parametro; non viene mai chiamato | Una dipendenza esiste solo perché la firma la richiede su questo percorso |
| Stub | Restituisce sempre la stessa risposta, qualunque sia l’input | Il test ha bisogno di un valore fisso e deterministico da una dipendenza |
| Spy | Registra le chiamate così il test può ispezionarle dopo | Il test deve confermare che un’interazione è avvenuta, senza configurarla prima |
| Fake | Una implementazione più piccola ma funzionante della cosa reale | Il test ha bisogno di un comportamento reale attraverso più chiamate, non un singolo valore fisso |
| Mock | Costruito con aspettative sulle chiamate valide, verificate mentre avvengono | Il test deve verificare la forma esatta di una chiamata: argomenti, numero, ordine |
gateway è stato uno stub, un fake e un mock in tre test della stessa classe. Il test double segue cosa verifica il test, non la dipendenza.
Quando i test double peggiorano i test
Mockare ogni collaboratore accoppia il test a come è scritto il codice, invece che a cosa fa. Aggiungi un test che verifica che total_with_tax venga chiamato esattamente una volta, poi modifica place_order per chiamarlo una seconda volta per il logging: il test si rompe, e niente che interessi a un utente è cambiato. Il test con il fake sopra non ha questo problema, perché verifica il risultato (due ID di addebito distinti, gli importi giusti registrati) invece della forma della chiamata.
Mockare una libreria che non controlli ha lo stesso problema a un livello più alto. Fai l’autospec diretto di un client di terze parti e ogni test che lo tocca dipende dalle firme dei metodi di quella libreria; un aggiornamento minore che rinomina un parametro rompe i test in tutta la suite. Avvolgi la libreria in una piccola interfaccia tua e sostituisci quella. Quando la libreria cambia, cambia solo il wrapper.
A volte il numero giusto di test double è zero. Se la dipendenza reale è già veloce e senza effetti collaterali, come una funzione pura o una connessione SQLite in memoria, sostituirla aggiunge indirezione e non cattura nulla. I test double servono per le dipendenze lente, remote, non deterministiche, o con effetti collaterali che non vuoi in un’esecuzione di test.
Come scegliere il test double giusto
Parti da cosa verifica il test, non da cosa sembra più sicuro simulare. Se serve un input prevedibile perché un’asserzione successiva tenga, usa uno stub. Per confermare che l’SMS sia partito, usa uno spy. Se la dipendenza deve comportarsi correttamente attraverso più chiamate, scrivi un fake una volta e riusalo ovunque quella dipendenza compaia. E quando un chiamante che passa gli argomenti sbagliati deve far fallire il test prima che fallisca in produzione, usa un mock autospecced.
Niente di tutto questo funziona su una classe che costruisce le proprie dipendenze internamente, perché non c’è un punto dove passare un double. È il problema che risolve la dependency injection, ed è la prima cosa da sistemare se una classe resiste a ogni double di questo articolo. Una volta che esiste il punto di innesto, decidi quale double appartiene a quale livello di test. Se lavori test-first, userai i test double da subito, perché il test esiste prima che ci sia codice reale da chiamare.