martinfowler.com logo Home Blog Articles Books About Me Contact Me ThoughtWorks

I mock non sono Stub


Martin Fowler
Il termine 'Mock Object' è divenuto un termine popolare per descrivere oggetti speciali che mimano oggetti reali allo scopo di eseguire test. La maggior parte degli ambienti hanno oggi dei framework che rendono facile la creazione di mock object. Ciò che non sempre si capisce, comunque, è che i mock object non sono che un caso particolare di oggetti di test, un caso che permette un diverso stile di test. In questo articolo spiegherò come lavorano i mock object, come aiutano nel test basato sulla verifica del comportamento e come la comunità attorno ad essi li usa per sviluppare uno stile diverso di test.

Ultimo aggiornamento: 2 gennaio 2007
Traduzione del 2 marzo 2007 (Articolo Originale)

Indice



Incontrai per la prima volta il termine "mock object" qualche anno fa nella comunità XP (eXtreme Programming, ndt). Da allora mi sono imbattuto più e più volte nei mock object. In parte perché molti dei migliori sviluppatori di mock object sono stati miei colleghi alla ThoughtWorks in diversi periodi. In parte perché li ritrovo sempre più nella letteratura sui test influenzata dall'XP.

Ma molto spesso trovo i mock object descritti malamente. In particolare li trovo sempre confusi con gli stub - un sistema diffuso per gli ambienti di test. Capisco questa confusione - io stesso li ho ritenuti simili per un po', ma varie conversazioni con gli sviluppatori di mock hanno fatto sì che una minima comprensione dei mock entrasse in quel guscio di tartaruga che ho per cranio.

Le differenze sono in realtà due separate. Da un lato c'è la differenza su come i risultati dei test vengono verificati: la distinzione tra verifica sullo stato e verifica sul comportamento. Dall'altra parte c'è la differenza sull'intera filosofia di come il test e il design si combinano tra loro, a cui mi riferisco qua come allo stile classico e mockico del Test Driven Development (TDD).

(Nella prima versione di questo saggio avevo realizzato che ci fosse una differenza, ma avevo considerato le due differenze come una sola. Da allora la mia conoscenza è migliorata e come risultato è giunta l'ora di aggiornare questo saggio. Se non avete letto il saggio precedente potete ignorare le miei dubbi, ho scritto questo saggio come se il precedente non fosse esistito. Ma se avete presente la versione vecchia potrebbe aiutarvi tenere conto che ho spezzato la vecchia dicotomia di test di stato e test di interazione nella dicotomia verifica di stato/comportamento e nella dicotomia TDD classico/mockico. Ho anche modificato il mio vocabolario per allinearlo a quello dell'imminente libro sui pattern xUnit di Gerard Meszaros.)



Test Regolari

Inizierò ad illustrare i due stili con un semplice esempio. (L'esempio è in Java, ma i principi hanno senso con qualsiasi altro linguaggio object-oriented). Si vuole prendere un oggetto Order e riempirlo da un oggetto Warehouse. Order è molto semplice, con solo un prodotto ed una quantità. Warehouse tiene l'inventario dei diversi prodotti. Quando si chiede ad un ordine di evadersi da un magazzino, ci dono due possibili risposte. Se c'è abbastanza prodotto nel magazzino per evadere l'ordine, l'ordine diventa evaso e l'ammontare a magazzino del prodotto viene diminuito dell'ammontare opportuno. Se non c'è abbastanza prodotto in magazzino allora l'ordine resta non evaso e non accade nulla nel magazzino.

Da questi due comportamenti si derivano un paio di test, i quali paiono test JUnit abbastanza convenzionali.
public class OrderStateTester extends TestCase {
private static String TALISKER = "talisker";
private static String HIGHLAND_PARK = "Highland Park";
private Warehouse warehouse = new WarehouseImpl();

protected void setUp() throws Exception {
warehouse.add(TALISKER, 50);
warehouse.add(HIGHLAND_PARK, 25);
}
public void testOrderIsFilledIfEnoughInWarehouse() {
Order order = new Order(TALISKER, 50);
order.fill(warehouse);
assertTrue(order.isFilled());
assertEquals(0, warehouse.getInventory(TALISKER));
}
public void testOrderDoesNotRemoveIfNotEnough() {
Order order = new Order(TALISKER, 51);
order.fill(warehouse);
assertFalse(order.isFilled());
assertEquals(50, warehouse.getInventory(TALISKER));
}

I test xUnit seguono una sequenza tipica di quattro fasi: preparazione, esecuzione, verifica, pulizia. In questo caso la fase di preparazione viene svolta in parte nel metodo setUp (preparazione del magazzino) ed in parte nel metodo di test (preparazione dell'ordine). La chiamata a order.fill è la fase di esecuzione. Qua è dove l'oggetto va a fare ciò che vogliamo testare. Le istruzione di assert sono la fase di verifica, per verificare su il metodo dell'esecuzione ha portato a termine correttamente il proprio compito. In questo caso non c'è alcuna fase esplicita di pulizia, la fa per noi implicitamente il garbage collector.

Durante la preparazione ci sono due tipologie di oggetto che stiamo mettendo assieme. Order è la classe che stima testando, ma affinché Order.fill funzioni abbiamo anche bisogno di un'istanza di Warehouse. In questa situazione è Order l'oggetto su cui ci stiamo svilppando per il test. Alla gente "orientata ai test" piace usare termini come oggetto-sotto-test o sistema-sotto-test per tali oggetti. Entrambi i termini sono angoscianti da dire, ma sono termini largamente accettati quindi mi tapperò il naso e li userò. Seguendo Meszaros userò Sistema Sotto Test, o o meglio l'abbreviazione SUT (System Under Test, NdT).

Così, per questo test ho bisogno del SUT (Order) e di un collaboratore (Warehouse). Ho bisogno del magazzino per due ragioni: la prima è perché il test possa funzionare veramente (in quanto Order.fill chiama i metodo di Warehouse) e poi ne ho bisogno per la verifica (in quanto uno degli effetti di Order.fill è un potenziale cambiamento allo stato del magazzino). Man mano che ci si addentra nell'argomento vedremo che si farà una grande distinzione fra SUT e i collaboratori. (Nella versione precedente di questo articolo mi riferivo al SUT come all'oggetto primario e ai collaboratori come agli oggetti secondari).

Questo stile di test usa la verifica di stato: significa che si determina se il metodo eseguito ha lavorato correttamente esaminando lo stato del SUT e dei suoi collaboratori dopo che il metodo è stato invocato. Come vedremo i mock object consentono un approccio diverso alla verifica.


Test con i Mock Object

Adesso prenderò in considerazione lo stesso comportamento e userò i mock object.In questo caso faccio uso della libreria jMock per definire i mock. jMock è una libreria di mock object Java. Ci sono altre librerie di mock object in giro, ma questa è una libreria aggiornata scritta dagli inventori della tecnica, così rappresenta da buon punto di partenza.
public class OrderInteractionTester extends MockObjectTestCase {
private static String TALISKER = "Talisker";

public void testFillingRemovesInventoryIfInStock() {
//setup - data
Order order = new Order(TALISKER, 50);
Mock warehouseMock = new Mock(Warehouse.class);

//setup - expectations
warehouseMock.expects(once()).method("hasInventory")
.with(eq(TALISKER),eq(50))
.will(returnValue(true));
warehouseMock.expects(once()).method("remove")
.with(eq(TALISKER), eq(50))
.after("hasInventory");

//exercise
order.fill((Warehouse) warehouseMock.proxy());

//verify
warehouseMock.verify();
assertTrue(order.isFilled());
}

public void testFillingDoesNotRemoveIfNotEnoughInStock() {
Order order = new Order(TALISKER, 51);
Mock warehouse = mock(Warehouse.class);

warehouse.expects(once()).method("hasInventory")
.withAnyArguments()
.will(returnValue(false));

order.fill((Warehouse) warehouse.proxy());

assertFalse(order.isFilled());
}

Concentratevi prima su  testFillingRemovesInventoryIfInStock, in quanto ho adottato un paio di scorciatoie nell'ultimo test.

Tanto per iniziare, la fase di preparazione è molto diversa. All'inizio è divisa in due parti: dati ed attese. La parte dati prepara gli oggetti con cui vogliamo lavorare, in questo senso è simile alla preparazione tradizionale. La differenza sta nella creazione degli oggetti. Il SUT è lo stesso - un Order. Ma il collaboratore non è un oggetto Warehouse, ma invece è un Warehouse mock - tecnicamente è un'istanza della classe Mock.

La seconda parte della preparazione crea le attese sul mock object. Le attese indicano quali metodi dovrebbero essere chiamati sui mock quando il SUT viene invocato.

Una volta che tutte le attese sono state sistemate si può invocare il SUT. Dopo l'invocazione si fa quindi la verifica, che ha due momenti. Si eseguono le assert sul SUT - proprio come prima. Però si verificano anche i mock - per verificare che siano stati invocati rispettando le attese.

Il punto fondamentale è il come si verifica che Order abbia fatto la cosa giusta interagendo con il Warehouse. Con la verifica di stato lo si fa verificando lo stato del Warehouse. I mock usano la verifica del comportamento, in cui si verifica se Order ha fatto le giuste invocazioni sul Warehouse. Lo si fa insegnando durante la fase di preparazione al mock cosa aspettarsi e chiedendo al mock di verificare se stesso durante la fase di verifica. Solo l'ordine viene controllato usando le assert, e se il metodo non dovesse modificare lo stato dell'oggetto non ci sarebbe alcuna assert.

Nel secondo test faccio un paio di cose diverse. Prima creo il mock diversamente, usando il metodo mock nel MockObjectTestCase anziché che nel costruttore. Questo è un metodo di supporto della libreria jMock, il che significa che non bisogna chiamare esplicitamente la verifica successivamente, ogni mock creato col metodo di supporto verrà automaticamente verificato alla fine del test. Avrei potuto farlo anche nel primo test, ma volevo far vedere la verifica in modo più esplicito per mostrare come funziona il test con i mock.

La seconda cosa diversa nel secondo caso di test è che ho rilassato i vincoli sulle attese usando withAnyArguments. La ragione di ciò è che il primo test controlla che il numero venga passato al Warehouse, quindi il secondo test non ha bisogno di ripetere quella verifica. Se si dovrà modificare successivamente la logica dell'ordine, allora solo un test fallirà, facilitando il lavoro per la modifica dei test. Come si può vedere, avrei potuto evitare withAnyArguments in quanto è il default.

Usare EasyMock

Ci sono parecchie librerie di mock object in giro. Una con cui ho lavorato un po', sia nella sua versione Java che .NET, è EasyMock. Anche EasyMock consente la verifica del comportamento, ma ha un paio di differenze di stile rispetto a jMock che val la pena discutere. Ecco di nuovo il solito test:

public class OrderEasyTester extends TestCase {
private static String TALISKER = "Talisker";

private MockControl warehouseControl;
private Warehouse warehouseMock;

public void setUp() {
warehouseControl = MockControl.createControl(Warehouse.class);
warehouseMock = (Warehouse) warehouseControl.getMock();
}

public void testFillingRemovesInventoryIfInStock() {
//setup - data
Order order = new Order(TALISKER, 50);

//setup - expectations
warehouseMock.hasInventory(TALISKER, 50);
warehouseControl.setReturnValue(true);
warehouseMock.remove(TALISKER, 50);
warehouseControl.replay();

//exercise
order.fill(warehouseMock);

//verify
warehouseControl.verify();
assertTrue(order.isFilled());
}

public void testFillingDoesNotRemoveIfNotEnoughInStock() {
Order order = new Order(TALISKER, 51);

warehouseMock.hasInventory(TALISKER, 51);
warehouseControl.setReturnValue(false);
warehouseControl.replay();

order.fill((Warehouse) warehouseMock);

assertFalse(order.isFilled());
warehouseControl.verify();
}
}

EsayMock utilizza la metafora registra/rispondi per impostare le attese. Per ogni oggetto di cui si vuole un mock bisogna creare un controllo e un mock object. Il mock soddisfa l'interfaccia dell'oggetto secondario, il controllo fornisce funzionalità aggiuntive. Per indicare un'attesa si invoca il metodo sul mock con gli argomenti che ci si aspetta. Lo si fa sul controllo se si vuole un valore di ritorno. Un volta finito di impostare le attese si chiama replay sul controllo - a questo punto il mock finisce la fase di registrazione ed è pronto a rispondere all'oggetto principale (SUT, ndT). Una volta finito si invoca la verify sul controllo.

Sembra che nonostante la gente rimanga inizialmente confusa dalla metafora registra/rispondi, ci si abitua poi velocemente. Si ha un vantaggio rispetto ai limiti di jMock nel senso che si fanno le vere chiamate ai metodi del mock piuttosto che specificando i nomi nelle stringhe. Ciò significa che si può usare il completamento del codice del proprio IDE e che ogni refactor sui nomi del metodo aggiorna automaticamente i test. La controparte è che non si possono avere vincoli più deboli.

Gli sviluppatori di jMock stanno lavorando su una nuova versione che permetterà di usare altre tecniche per consentire l'uso delle chiamate dei metodi.


La Differenza tra Mocks e Stubs

Quando furono introdotti per la prima volta, molta gente confondeva facilmente i mock object con la nozione comune di usare stub per i test. Da allora sembrerebbe che la gente capisca meglio la differenza (e spero che la prima versione di questo articolo sia servita in proposito). Comunque per capire appieno il modo in cui la gente usa i mock è importante capire i mock e gli altri tipi di doppioni (doubles, NdT) di test. ("doppioni"? Non preoccupatevi se per voi è un termine nuovo, aspettate pochi paragrafi e diventerà chiaro.)

Quando si fanno test di questo tipo ci si concentra su un elemento del software alla volta - da cui il termine comune unit test. Il problema è che per far funzionare una singola unit, si ha spesso bisogno di altre unit - vedi il bisogno di avere un magazzino nel nostro esempio.

Nei due stili di test mostrati sopra, il primo caso usa un oggetto magazzino reale e il secondo un magazzino mock, che naturalmente non è un oggetto reale. Usare i mock è un modo per non usare un oggetto reale nel test, ma ci sono altre forme di utilizzo di oggetti non reali in test come questi.
Il vocabolario per parlare di questo argomento diventa alla svelta confuso - si usano tutti i tipi di parole: stub, mock, fake, dummy. Per questo articolo intendo seguire il vocabolario sull'imminente libro di Gerard Meszaros. Non è quello utilizzato da tutti, ma penso sia un buon vocabolario ed essendo il saggio mio decido io quali termini usare.
Meszaros usa il termine Doppione di Test (Test Double, NdT) come il termine generico per ogni tipo di oggetto sostitutivo usato al posto di un oggetto reale a scopo di test. Il nome deriva dalla nozione di Stunt Double usato nei film. (L'obiettivo era di evitare di usare uno dei nomi già largamente utilizzati.) Meszaros definisce quindi quattro tipi particolari di doppioni:
Di questi tipi di doppioni, solo con i mock è possibile praticare la verifica del comportamento. Con gli altri doppioni si può, e di solito è così, usare la verifica di stato. I mock in realtà si comportano come gli altri doppioni durante la fase di esecuzione, in quanto devono far credere al SUT si parlare con i veri collaboratori. Molte persone usano i doppioni per i test solo se è scomodo lavorare con l'oggetto reale. Un caso più comune per un doppione di test sarebbe se si volesse inviare una mail nel caso in cui fallisse l'evasione di un ordine. Il problema è che non vogliamo inviare veri messaggi di posta verso i nostri clienti durante i test. Così invece si crea un doppione di test del nostro sistema di mail, uno che possiamo controllare e manipolare.
Ora risulta chiara la differenza tra mock e stub. Se scrivessimo un test per questo comportamento di invio posta, potremmo scrivere un semplice stub come questo.
public interface MailService {
public void send (Message msg);
}
public class MailServiceStub implements MailService {
private List<Message> messages = new ArrayList<Message>();
public void send (Message msg) {
messages.add(msg);
}
public int numberSent() {
return messages.size();
}
}

Possiamo poi usare la verifica di stato sullo stub così.

class OrderStateTester...
public void testOrderSendsMailIfUnfilled() {
Order order = new Order(TALISKER, 51);
MailServiceStub mailer = new MailServiceStub();
order.setMailer(mailer);
order.fill(warehouse);
assertEquals(1, mailer.numberSent());
}

Naturalmente questo test è molto semplice - so solo che è stato inviato un messaggio. Non abbiamo testato se sia stato inviato alla persona corretta o con il giusto contenuto, ma spiega bene il punto.

Usando i mock questo test sarebbe molto diverso.

class OrderInteractionTester...
public void testOrderSendsMailIfUnfilled() {
Order order = new Order(TALISKER, 51);
Mock warehouse = mock(Warehouse.class);
Mock mailer = mock(MailService.class);
order.setMailer((MailService) mailer.proxy());

mailer.expects(once()).method("send");
warehouse.expects(once()).method("hasInventory")
.withAnyArguments()
.will(returnValue(false));

order.fill((Warehouse) warehouse.proxy());
}
}

In entrambi i casi sto usando un doppione di test del servizio di posta reale. C'è anche una differenza in quanto lo stub usa la verifica di stato mentre il mock usa la verifica di comportamento.

Al fine di usare la verifica di stato sullo stub c'è bisogno di qualche metodo aggiuntivo sullo stub per permettere la verifica. Come risultato lo stub implementa MailService ma aggiunge metodi extra.
I mock object usano sempre la verifica di comportamento, uno stub può prendere entrambe le direzioni. Meszaros chiama gli stub che usano la verifica di comportamento Spie di Test (Test Spy). La differenza è su come esattamente il doppione gira e viene verificato e lascerò a voi esplorarla da soli.

Test Classico e Mockico

Ora sono arrivato al punto di poter esplorare la seconda dicotomia: quella tra il TDD classico e mockico. Il punto saliente qua è quando usare un mock (o un altro doppione).
Lo stile del TDD classico  è di usare oggetti reali quando possibile ed un doppione se è scomodo usare la cosa vera. Così un TDD classico userebbe un magazzino reale ed un doppione per il servizio di posta. Quale tipo di doppione non ha molta importanza.
Un praticante del TDD mockico, comunque, userà sempre un mock per ogni oggetto con un comportamento interessante. In questo caso sia per il magazzino che per il servizio di posta.
Sebbene i diversi framework di mock siano stati progettati con in mente il test mockico, molti classicisti li trovano utili per creare doppioni.
Un'importante ramificazione dello stile mockico è il Behavior Driven Development (BDD, Sviluppo Guidato dal Comportamento, NdT). Il BDD fu sviluppato inizialmente dal mio collega Dan North come una tecnica per far capire meglio alla gente il TDD mettendo l'attenzione su come il TDD sia una tecnica di progetto. Ciò porta a rinominare i test comportamenti per esplorare meglio dove il TDD aiuta a pensare a che cosa un oggetto ha bisogno di fare. Il BDD intraprende l'approccio mockico, ma si estende oltre, sia per lo stile dei nomi cge per il suo desiderio di integrare l'analisi all'interno della sua stessa tecnica. Non voglio andare ulteriormente oltre qui, in quanto la sola rilevanza per questo articolo è che il BDD sia un'altra variazione sul TDD che tende ad usare il test mockico. Lascio a voi seguire il link per ulteriori informazioni.

Scegliere tra le Differenze

In questo articolo ho spiegato un paio di differenze: verifica di stato o di comportamento / TDD classico e mockico. Quali sono gli elementi da tenere in mente mentre si sceglie tra loro? Iniziamo con la scelta di verifica di stato o di comportamento.
La prima cosa da considerare è il contesto. Stiamo pensando ad una collaborazione semplice, come quelle di ordine e magazzino o ad una complessa, come quella di ordine e servizio di posta?
Se è una collaborazione semplice allora la scelta è semplice. Se sono un classicista del TDD non uso un mock, uno stub o qualche altro tipo di doppione. Uso l'oggetto reale e la verifica di stato. Se sono un mockista del TDD allora uso un mock e la verifica di comportamento. Non c'è da prendere nessuna decisione.
Se è una collaborazione complessa, allora non c'è alcuna decisione se sono un mockico - uso semplicemente i mock e la verifica di comportamento. Se sono un classico allora devo fare una scelta, ma non è un problema quale usare. Di solito i classicisti decideranno caso per caso, usando la strada più semplice per ogni situazione.
Dunque come abbiamo visto, la verifica di stato oppure di comportamento non sono una gran scelta. La vera scelta è tra il TDD classico e mockico. Come abbiamo capito le caratteristiche della verifica di stato e di comportamento influenzano la discussione, ed è lì che concentrerò le mie energie.
Ma prima, lasciatemi proporre un caso limite. Ogni tanto ci si trova in mezzo a cose per cui risulta veramente difficile verificare lo stato, anche se non sono collaborazioni complesse. Un ottimo esempio di ciò sono le cache. Il succo di una cache è che non puoi capire dal suo stato se la cache ha i dati o no - questo è un caso dove la verifica di comportamento sarebbe la scelta saggia anche per un cocciuto classicista del TDD. Sono sicuro che ci sono altre eccezioni in entrambe le direzioni.
Man mano che ci addentriamo nella scelta classica/mockica, ci sono un sacco di fattori da considerare, perciò ho li ho spezzati in macro gruppi.

Guidare il TDD

I mock object escono dalla comunità XP, e una delle caratteristiche principali di XP è l'enfasi sul Test Driven Development - in cui il design del sistema evolve attraverso iterazioni guidate dalla stesura di test.

Dunque non è una sorpresa che i mockisti parlino in particolare dell'effetto del test mockico sul design. In particolare sostengono uno stile chiamato sviluppo guidato dalle esigenze. Con questo stile si inizia a sviluppare una storia scrivendo i primi test per l'esterno del sistema, facendo qualche interfaccia di oggetto per il SUT. Pensando alle verifiche sui collaboratori, si esplorano le interazioni tra il SUT e i suoi vicini - progettando in modo efficiente l'interfaccia di confine del SUT.
Una volta finito il primo lancio di test, le attese sui mock forniscono una specifica per il passo successivo ed un punto di inizio per i test. Si convertono le attese in test su un collaboratore e si ripete il processo percorrendo la strada all'interno del sistema, un SUT alla volta. Questo stile viene anche chiamato dentro-fuori (outside-in, NdT), un nome molto appropriato. Funziona bene con sistemi stratificati. Si inizia programmando la UI usando strati di mock al di sotto. Quindi si scrivono test per i livelli inferiori, entrando passo passo nel sistema uno strato alla volta. Questo è un approccio molto strutturato e controllato, quello che molta gente ritiene d'aiuto per guidare i neofiti all'OO e al TDD.
Il TDD classico non fornisce una simile guida. Si possono fare approcci simili usando metodi fake invece dei mock. Per farlo ogni volta che c'è bisogno di qualcosa da un collaboratore si cabla il codice che fornisce esattamente la risposta che il test richiede affinché il SUT funzioni. Poi una volta che il semaforo è verde si rimpiazza la risposta cablata con il codice vero e proprio.
Ma il TDD classico può fare anche altre cose. Uno stile diffuso è il dentro-fuori (middle-out, NdT). In questo stile si sceglie una funzionalità e si decide di cosa c'è bisogno nel dominio affinché questa funzioni. Si fa in modo che gli oggetti del dominio facciano ciò che serve e una volta che funzionano ci si stratifica sopra la UI. Procedendo così si potrebbe non dover mai fare alcun fake. A molta gente ciò piace perché concentra l'attenzione prima sul modello del dominio, che evita di far sì che le logiche di dominio invadano la UI.
Vorrei stressare che entrambi gli approcci classico e mockico fanno ciò una storia per volta. C'è una scuola di pensiero che costruisce applicazioni strato dopo strato senza iniziare un nuovo strato finché un altro no sia completo. Classicist e mockisti tendono ad avere un background agile e preferiscono iterazioni piccole. Come risultato lavorano funzionalità per funzionalità anziché strato per strato.

Preparazione delle Fixture

Nel TDD classico c'è bisogno di creare non soltanto il SUT ma anche tutti i collaboratori che il SUT richiede in risposta ai test. Mentre nell'esempio ci sono solo un paio di oggetti, i test reali coinvolgono spesso un grande numero di oggetti secondari. Di solito questi oggetti vengono creati e distrutti ad ogni esecuzione dei test.

I test mockici, comunque, hanno bisogno solo di creare il SUT e i mock per i vicini più prossimi. Ciò può evitare del lavoro richiesto per costruire funzionalità complesse. (Almeno in teoria. Ho sentito racconti di preparazioni di mock piuttosto complesse, ma ciò potrebbe essere dovuto all'utilizzo non corretto dello strumento.)

In pratica, i classicisti tendono a riutilizzare funzionalità complesse il più possibile. Il modo più semplice è mettere il codice di preparazione della fixture nel metodo di setup delle xUnit. Le fixture più complicate devono essere usate da diverse classi di test, così in questo caso si creano classi speciali generate dalle fixture. Le chiamo di solito Object Mothers (Madri degli Oggetti), basandomi su una convenzione utilizzata su uno dei primi progetti XP alla ThoughtWorks. Usare le madri diventa indispensabile nei test classici di grandi dimensioni, ma le madri sono codice aggiuntivo che deve essere mantenuto ed ogni modifica alle madri può avere un effetto onda attraverso i test. Ci potrebbe anche essere un costo di performance nel preparare la fixture - sebbene non abbia mai sentito che possa essere un problema se viene fatto in modo corretto. Creare la maggior parte degli oggetti fixture costa poco, per quelli che non lo sono vengono creati doppioni.
Come risultato, ho sentito entrambi gli stili accusare l'altro di scrivere troppo codice. I mockisti dicono che creare le fixture richieda un grande sforzo, invece i classicisti dicono che queste possono essere riutilizzate e che invece bisogna scrivere dei mock diversi per ogni test.

Isolamento dei Test

Se si infila un bug in un sistema col test mockico, in genere accadrà che falliscono solo i test del SUT che contiene il baco. Nell'approccio classico invece tutti i test degli oggetti client posso altresì fallire, il che porta a fallimenti dove l'oggetto bacato viene utilizzato come collaboratore nel test di altri oggetti. Come effetto una falla in un oggetto largamente usato provoca un'onda di test falliti in tutto il sistema.

I mockist lo considerano un argomento rilevante; si finisce a fare un sacco di debug al fine di trovare l'errore vero e correggerlo. Comunque i classicisti non lo considerano una fonte di problemi. Di solito il colpevole è facile da individuare guardando quali test falliscono e gli sviluppatori possono distinguere i fallimenti derivati dalla falla originale. Inoltre, se si sta testando regolarmente (some si dovrebbe), allora si sa che il fallimento è stata provocato dalle ultime cose che si sono modificate, così non è difficile trovare la falla.

Un fattore che può essere importante qua è la granularità dei test. Siccome i test classici mettono in opera gli oggetti reali, si individua spesso un test singolo come test principale di un assemblato di oggetti, piuttosto che di uno solo. Se tale assemblato riguarda molti oggetti , allora può essere molto più difficile trovare la vera fonte del bug. Quello che succede qua è che i test hanno granularità troppo grossa.

Accede abbastanza spesso che i test mockici siano meno esposti a questo problema, perché la convenzione dice di fare mock di tutti gli oggetti tranne del principale, il che rende chiaro che servono test più fini per i collaboratori. Detto ciò, è anche vero che l'uso di test con granularità eccessivamente grossa non è necessariamente un fallimento del test classico come metodologia, ma piuttosto è un fallimento nell'uso corretto della tecnica. Una buona regola è di cautelarsi di separare test a grana fine per ogni classe. Mentre gli assemblaggi sono talvolta appropriati, dovrebbero essere limitati a solo pochi oggetti - non più di mezza dozzina. In più, se ci si trova con un problema di debug dovuto a test a grana eccessivamente grossa, si dovrebbe debuggare in modo guidato da test, creando via via test a grana più fine.

In essenza i test xunit classici non sono solo unit test, ma anche mini test di integrazione. Come risultato, a molta gente piace il fatto che i test lato client possano scovare errori che i test di un oggetto potrebbero aver mancato, in particolare il test di quelle aree in cui le classi interagiscono. I testi mockici perdono tale qualità. Inoltre si corre il rischio di sbagliare le attese nei test mockici, ottenendo una barra verde che però nasconde errori intrinsechi.

Ed è qui che sottolineo il fatto che qualsiasi stile si scelga di usare, bisogna combinarlo con con dei test di accettazione a grana più grossa che operino attraverso il sistema come un tuttuno. Mi sono spesso visto in progetti che sono arrivati tardi ad usare test di accettazione e se ne sono pentiti.

Accoppiamento tra Test e Implementazione

Quando si scrive un test mockico, si stanno testando le chiamate al confine del SUT per assicurarsi che parli correttamente ai suoi fornitori. Un test classico si preoccupa solo dello stato finale - non di come ci si giunga. I test mockici sono dunque più legati all'implementazione di un metodo. Cambiare la natura delle chiamate ai collaboratori di solito fa fallire un test mockico.

Questo accoppiamento porta ad un paio di problemi. Quello più importante è l'effetto del Test Driven Development. Con il test mockico, scrivere il test fa pensare all'implementazione del comportamento - infatti i mockisti lo vedono come un vantaggio. I classicisti, comunque, credono che sia importante pensare solo a cosa succede dall'interfaccia esterna e lasciare tutte le considerazioni dell'implementazione a dopo aver scritto il test.

L'accoppiamento all'implementazione influisce anche sul refactoring in quanto è più probabile che i cambiamenti nell'implementazione facciano fallire i test.

Tutto ciò può essere peggiorato dalla natura del toolkit del mock. Spesso glis trumenti per i mock specificano chiamate a metodi e corrispondenze di parametri, anche quando non sono rilevanti in un test particolare. Uno degli obiettivi del tollkit jMock è di essere più flessibile nelle specifiche delle attese affinché possono essere più deboli nelle pati in cui non sono importanti, al costo di usare stringhe che possono rendere il refactor più intricato.

Stile di Design

Secondo me uno degli  aspetti più affascinanti di questi stili di test è il come influenzano le scelte di design. Avendo parlato con entrambi i tipi di utenti mi sono fatto l'idea di qualche differenza tra il design che gli stili comportano, ma sono sicuro che sto solo graffiando la superficie.

Ho già menzionato la differenza sull'affrontare gli strati. Il test mockico favoreggia l'approccio fuori-dentro mentre gli sviluppatori che preferiscono lo stile di modellare il dominio esternamente preferiscono il test classico.

Ad un livello più basso ho notato che i mockisti tendono a rifuggire da metodi che ritornano valori, a favore di metodi che agiscono sopra ad oggetti contenitore. Si consideri il caso in cui bisogna raccogliere informazioni da un gruppo di oggetti per creare una stringa riassuntiva. Un modo diffuso di farlo è di fare che il metodo chiami sui vari oggetti i metodi che restituiscono una stringa e poi assembli le stringhe risultanti in una variabile temporanea. Un mockista preferirebbe passare un buffer di stringa attraverso i diversi oggetti ed far sì che siano loro ad aggiungere le diverse stringhe al buffer - trattando il buffer di stringa come un parametro di raccolta.

I mockisti parlano più di evitare 'trenini' - catene di metodi nello stile getThis().getThat().getTheOther(). Evitare le catene di metodi è conosciuto come la legge di Demetrio. Anche se le catene di metodi sono una puzza, lo è anche il problema inverso di oggetti intermedi soffocati con metodi di rinvio (forward, NdT). (Ho sempre sentito che sarei stato più a mio agio con la legge di Demetrio se si fosse chiamata il suggerimento di Demetrio.)
Una delle cose più difficili da capire per la gente nel design OO è il principio "Dillo Non Chiederlo", che incoraggia a dire ad un oggetto di fare qualcosa piuttosto che ad estrarre i dati da un oggetto e farlo nel codice client. I mockisti dicono che usare i test mock aiuta a favorire ciò e ad evitare i getter coriandoli che pervadono troppo codice in questi giorni. I classicisti argomentano che ci sono molti altri modi di farlo. Un fatto riconosciuto con la verifica basata sullo stato è che può portare a creare metodi di interrogazione solo a supporto delle verifiche. Non è mai bello aggiungere metodi ad un API solo per i test, usare la verifica del comportamento evita questo problema. La contro argomentazione a ciò è che di solito queste modifiche sono molto piccole.
I mockisti favoriscono le interfacce di ruolo ed asseriscono che usare questo stile di test incoraggia di più le interfacce di ruolo, in quanto ogni collaborazione viene mockata separatamente ed è quindi più facile che diventi un'interfaccia di ruolo. Quindi nel mio esempio di prima, l'uso un buffer di stringa per generare un report, un mockista tenderebbe ad inventare un particolare ruolo che ha senso in quel dominio che potrebbe essere svolto da un buffer di stringa.
È importante ricordare che questa differenza nello stile di design è una motivazione chiave per la maggior parte dei mockisti. Le origini del TDD erano il desiderio di avere forti test di regressione automatica che supportassero il design evolutivo. Strada facendo, i suoi praticanti scoprirono che scrivere prima i test apporta un miglioramento significativo al processo di design. I mockcisti hanno una forte idea di quale tipologia di design sia un buon design e hanno sviluppato le librerie di mock in primo luogo per favorire la gente a sviluppare secondo questo stile di design.

Quindi dovrei essere un classicista o un mockista?

La trovo una domanda difficile a cui rispondere con sicurezza. Personalmente sono sempre stato un vecchio classicista del TDD e finora non vedo alcuna ragione per cambiare. Non vedo nessun beneficio irresistibile per nel TDD mockico, e sono spaventato delle conseguenze di accoppiare il test all'implementazione.

Ciò mi ha particolarmente colpito mentre osservavo un programmatore mockico. Mi piace molto il fatto che mentre si scrivono i test ci si concentra sui risultati del comportamento, non su come lo si fa. Un mockista pensa continuamente a come il SUT dovrà essere implementato mentre scrive le attese. Ciò mi sembra innaturale.

Soffro pure lo svantaggio di non aver provato il TDD mockico su qualcosa al di là di giocattoli. Come ho imparato dal Test Driven Development stesso, è spesso difficile giudicare una tecnica senza averla provata seriamente. Conosco molti bravi sviluppatori che sono convinti mockisti molto felici. Così sebbene io sia ancora un convinto classicista, ho preferito presentare entrambi gli argomenti il più equamente possibile in modo che ci si possa fare una propria idea.
Dunque se il test mockico vi suona invitante vi suggerirei di provarlo. Vale la pena in particolare se avete problemi in qualche area per cui il TDD mockico è nato per migliorarla. Vedo lì due grandi aree. Una è quando si trascorre troppo tempo a debuggare i test falliti perché non falliscono in modo chiaro dicendo deve sta il problema. (Si può anche migliorare qua usano il TDD classico su aggregati più fini.) La seconda area è dove gli oggetti non contengono abbastanza comportamento ed il test mockico potrebbe incoraggiare il team di sviluppo a creare oggetti più ricchi di comportamento.

Pensieri Conclusivi

Nell'ambito dello unit test sono cresciuti i framework xunit e il Test Driven Develpment; sempre più gente sta scoprendo i mock object. Un sacco di volte la gente impara qualche cosa sul framework mock senza capire completamente la divisione classico/mockico che c'è sotto. Qualsiasi sia lo schieramento che si sceglie, credo sia utile capire la differenza di visione. Mentre non bisogna essere un mockico per trovare il framework mock utile, è utile capire il pensiero che guida molte decisioni di design del software.

Il fine di questo articolo era ed è di estrarre le differenze e di spiegare i vantaggi svantaggi di entrambi. C'è molto di più sul pensiero mockico di quanto io abbia avuto tempo di esporre, in particolare le sue conseguenze sullo stile di design. Spero che nei prossimi anni vedremo più scritti sull'argomento e che ciò approfondirà la nostra conoscenza sulle affascinanti conseguenze di scrivere i test prima del codice.



Ulteriori Letture

Per una panoramica sul pensiero della pratica del test xunit, tenete d'occhio l'imminente libro di Gerard Meszaros (rinuncia: è nelle mie serie).

Egli tiene anche un sito con i pattern estratti dal libro.

Per trovare di più sul TDD il primo posto da vedere è il libro di Kent.

Per trovare di più sollo stile mockico di test, il primo posto da guardare è mockobjects.com dove Steve Freeman e Nat Price difendono il punto di vista mockico con articoli ed un blog proficuo. In particolare si legga l'eccellente articolo dell'OOPSLA. Per saperne di più sul Behaviour Driven Development, una diversa derivazione del TDD che è in stile molto mockico, iniziate dall'introduzione di Dan North.
Si può trovare di più su queste tecniche guardando i siti degli strumenti jMock, nMock, EasyMock, e .NET EasyMock. (Ci sono altri strumenti mock là fuori, non si consideri questa lista completa.)
L'XP200 ha visto l'articolo originale sui mock object, ma orami è un po' datato.

Revisioni Significative

02 gennaio 07: Suddivisa la distinzione originale tra test basato sullo stato e test basto sul comportamento in due: verifica di stato e di comportamento e TDD classico e mockico. Ho fatto anche diverse modifiche di vocabolario per allinearlo a quello del libro di Gerard Meszaros sui pattern xunit.

08 luglio 04: Prima pubblicazione


Tradotto da Marco Perrando.

Nota sul Traduttore: Marco Perrando lavora nel campo dello sviluppo software come sviluppatore e progettista da oltre 10 anni. Attualmente lavora per la SIR - Soluzioni in Rete di Genova.


martinfowler.com logo thoughtworks logo

© Copyright Martin Fowler, tutti i diritti riservati