Quando dovrei deridere?


138

Ho una conoscenza di base di oggetti finti e falsi, ma non sono sicuro di avere la sensazione di quando / dove usare il derisione, specialmente come si applicherebbe a questo scenario qui .


Raccomando solo di deridere le dipendenze fuori processo e solo quelle, interazioni osservabili esternamente (server SMTP, bus dei messaggi, ecc.). Non deridere il database, è un dettaglio di implementazione. Maggiori informazioni qui: enterprisecraftsmanship.com/posts/when-to-mock
Vladimir

Risposte:


122

Un unit test dovrebbe testare un singolo codepath attraverso un singolo metodo. Quando l'esecuzione di un metodo passa al di fuori di quel metodo, in un altro oggetto e viceversa, si ha una dipendenza.

Quando testate quel percorso di codice con la dipendenza effettiva, non state testando l'unità; stai test di integrazione. Sebbene sia buono e necessario, non è un test unitario.

Se la tua dipendenza è difettosa, il test potrebbe essere influenzato in modo tale da restituire un falso positivo. Ad esempio, è possibile passare alla dipendenza un valore nullo imprevisto e la dipendenza potrebbe non essere impostata su null come è documentato. Il test non rileva un'eccezione dell'argomento null come dovrebbe e il test ha esito positivo.

Inoltre, potrebbe essere difficile, se non impossibile, ottenere in modo affidabile l'oggetto dipendente per restituire esattamente ciò che si desidera durante un test. Ciò include anche il lancio di eccezioni previste nei test.

Una finta sostituisce quella dipendenza. Impostate le aspettative sulle chiamate all'oggetto dipendente, impostate i valori di ritorno esatti che dovrebbe fornirvi per eseguire il test desiderato e / o quali eccezioni lanciate in modo da poter testare il codice di gestione delle eccezioni. In questo modo è possibile testare facilmente l'unità in questione.

TL; DR: deride ogni dipendenza toccata dal test unitario.


164
Questa risposta è troppo radicale. I test unitari possono e devono esercitare più di un singolo metodo, purché appartengano tutti alla stessa unità coesiva. Fare diversamente richiederebbe fin troppo beffardo / finto, portando a test complicati e fragili. Solo le dipendenze che non appartengono realmente all'unità sottoposta a test dovrebbero essere sostituite tramite derisione.
Rogério,

10
Anche questa risposta è troppo ottimista. Sarebbe meglio se incorporasse le mancanze di @ Jan degli oggetti finti.
Jeff Axelrod,

1
Non è forse più un argomento per iniettare dipendenze per i test piuttosto che per le beffe in particolare? Potresti praticamente sostituire "mock" con "stub" nella tua risposta. Sono d'accordo che dovresti deridere o stub le dipendenze significative. Ho visto un sacco di codice fittizio che sostanzialmente finisce per reimplementare parti degli oggetti derisi; le derisioni non sono certamente un proiettile d'argento.
Draemon

2
Deridete ogni dipendenza toccata dal vostro test unitario. Questo spiega tutto.
Teoman Shipahi,

2
TL; DR: deride ogni dipendenza toccata dal test unitario. - questo non è davvero un ottimo approccio, dice lo stesso mockito - non deridere tutto. (downvoted)
p_champ

167

Gli oggetti simulati sono utili quando si desidera testare le interazioni tra una classe sotto test e una particolare interfaccia.

Ad esempio, vogliamo testare quel metodo sendInvitations(MailServer mailServer)chiamate MailServer.createMessage()esattamente una volta e anche chiamate MailServer.sendMessage(m)esattamente una volta, e nessun altro metodo viene chiamato MailServersull'interfaccia. Questo è quando possiamo usare oggetti finti.

Con oggetti simulati, invece di passare un MailServerImpltest reale o test TestMailServer, possiamo passare un'implementazione simulata MailServerdell'interfaccia. Prima di passare un finto MailServer, lo "alleniamo", in modo che sappia quale metodo chiama aspettarsi e quali valori di ritorno restituire. Alla fine, l'oggetto simulato afferma che tutti i metodi previsti sono stati chiamati come previsto.

Questo suona bene in teoria, ma ci sono anche alcuni aspetti negativi.

Difetti finti

Se si dispone di un framework simulato, si è tentati di utilizzare l'oggetto simulato ogni volta che è necessario passare un'interfaccia alla classe sotto il test. In questo modo finisci per testare le interazioni anche quando non è necessario . Sfortunatamente, il test indesiderato (accidentale) delle interazioni è negativo, perché quindi stai testando che un particolare requisito sia implementato in un modo particolare, invece che l'implementazione abbia prodotto il risultato richiesto.

Ecco un esempio in pseudocodice. Supponiamo di aver creato una MySorterclasse e vogliamo testarla:

// the correct way of testing
testSort() {
    testList = [1, 7, 3, 8, 2] 
    MySorter.sort(testList)

    assert testList equals [1, 2, 3, 7, 8]
}


// incorrect, testing implementation
testSort() {
    testList = [1, 7, 3, 8, 2] 
    MySorter.sort(testList)

    assert that compare(1, 2) was called once 
    assert that compare(1, 3) was not called 
    assert that compare(2, 3) was called once 
    ....
}

(In questo esempio supponiamo che non sia un particolare algoritmo di ordinamento, come l'ordinamento rapido, che vogliamo testare; in tal caso, quest'ultimo test sarebbe effettivamente valido.)

In un esempio così estremo è ovvio il motivo per cui quest'ultimo esempio è sbagliato. Quando cambiamo l'implementazione di MySorter, il primo test fa un ottimo lavoro assicurandoci che riordiniamo ancora correttamente, che è l'intero punto dei test: ci permettono di cambiare il codice in modo sicuro. D'altra parte, quest'ultimo test si interrompe sempre ed è attivamente dannoso; ostacola il refactoring.

Deride come tronconi

I framework simulati spesso consentono anche un utilizzo meno rigoroso, in cui non è necessario specificare esattamente quante volte devono essere chiamati i metodi e quali parametri sono previsti; consentono di creare oggetti simulati che vengono utilizzati come tronconi .

Supponiamo di avere un metodo sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer)che vogliamo testare. L' PdfFormatteroggetto può essere utilizzato per creare l'invito. Ecco il test:

testInvitations() {
   // train as stub
   pdfFormatter = create mock of PdfFormatter
   let pdfFormatter.getCanvasWidth() returns 100
   let pdfFormatter.getCanvasHeight() returns 300
   let pdfFormatter.addText(x, y, text) returns true 
   let pdfFormatter.drawLine(line) does nothing

   // train as mock
   mailServer = create mock of MailServer
   expect mailServer.sendMail() called exactly once

   // do the test
   sendInvitations(pdfFormatter, mailServer)

   assert that all pdfFormatter expectations are met
   assert that all mailServer expectations are met
}

In questo esempio, non ci interessa davvero l' PdfFormatteroggetto, quindi lo addestriamo per accettare tranquillamente qualsiasi chiamata e restituire alcuni valori di ritorno fissi ragionevoli per tutti i metodi che si sendInvitation()verificano a questo punto. Come abbiamo creato esattamente questo elenco di metodi per allenarci? Abbiamo semplicemente eseguito il test e continuato ad aggiungere i metodi fino al superamento del test. Si noti che abbiamo addestrato lo stub a rispondere a un metodo senza avere la minima idea del perché debba chiamarlo, abbiamo semplicemente aggiunto tutto ciò di cui si lamentava il test. Siamo felici, il test ha superato.

Ma cosa succede dopo, quando cambiamo sendInvitations(), o qualche altra classe che sendInvitations()usa, per creare PDF più fantasiosi? Il nostro test fallisce improvvisamente perché ora PdfFormattervengono chiamati più metodi e non ci siamo allenati con il nostro stub ad aspettarli. E di solito non è solo un test che fallisce in situazioni come questa, è qualsiasi test che capita di usare, direttamente o indirettamente, il sendInvitations()metodo. Dobbiamo correggere tutti quei test aggiungendo altri corsi di formazione. Si noti inoltre che non è possibile rimuovere i metodi non più necessari, poiché non sappiamo quali di essi non siano necessari. Ancora una volta, ostacola il refactoring.

Inoltre, la leggibilità del test ha sofferto terribilmente, c'è un sacco di codice lì che non abbiamo scritto perché volevamo, ma perché dovevamo; non siamo noi che vogliamo quel codice lì. I test che utilizzano oggetti simulati sembrano molto complessi e sono spesso difficili da leggere. I test dovrebbero aiutare il lettore a capire come utilizzare la classe sotto il test, quindi dovrebbero essere semplici e chiari. Se non sono leggibili, nessuno li manterrà; infatti, è più facile eliminarli che mantenerli.

Come risolverlo? Facilmente:

  • Prova a usare classi reali invece di beffe quando possibile. Usa il reale PdfFormatterImpl. Se non è possibile, cambia le classi reali per renderlo possibile. Non essere in grado di utilizzare una classe nei test di solito indica alcuni problemi con la classe. Risolvere i problemi è una situazione vantaggiosa per tutti: hai corretto la classe e hai un test più semplice. D'altra parte, non risolverlo e usare derisioni è una situazione senza vincita: non hai riparato la classe reale e hai test più complessi e meno leggibili che ostacolano ulteriori rifatturazioni.
  • Prova a creare una semplice implementazione di test dell'interfaccia invece di deriderla in ogni test e usa questa classe di test in tutti i tuoi test. Crea TestPdfFormatterche non fa nulla. In questo modo puoi cambiarlo una volta per tutti i test e i tuoi test non sono ingombri di lunghe configurazioni in cui alleni i tuoi stub.

Tutto sommato, gli oggetti finti hanno il loro uso, ma quando non vengono usati con attenzione, spesso incoraggiano cattive pratiche, testando i dettagli dell'implementazione, ostacolano il refactoring e producono test difficili da leggere e difficili da mantenere .

Per ulteriori dettagli sulle carenze delle derisioni, vedere anche Oggetti simulati: carenze e casi d'uso .


1
Una risposta ben ponderata e per lo più concordo. Direi che poiché i test unitari sono test in white box, dover cambiare i test quando si cambia l'implementazione per inviare PDF più elaborati potrebbe non essere un onere irragionevole. A volte le beffe possono essere un modo utile per implementare rapidamente gli stub invece di avere molta piastra della caldaia. In pratica, tuttavia, sembra che il loro uso non sia limitato a questi semplici casi.
Draemon

1
non è il punto di una derisione è che i tuoi test sono coerenti, che non devi preoccuparti di deridere su oggetti le cui implementazioni cambiano continuamente possibilmente da altri programmatori ogni volta che esegui il test e ottieni risultati di test coerenti?
Positivo

1
Punti molto validi e pertinenti (soprattutto sulla fragilità dei test). Quando ero più giovane usavo molto i mock, ma ora considero il test unitario che dipende fortemente dai mock come potenzialmente usa e getta e mi concentro maggiormente sui test di integrazione (con componenti reali)
Kemoda,

6
"Non poter utilizzare una classe nei test di solito indica alcuni problemi con la classe." Se la classe è un servizio (es. Accesso al database o proxy al servizio web), dovrebbe essere considerata come una dipendenza esterna e derisa /
cancellata

1
Ma cosa succede dopo, quando cambiamo sendInvitations ()? Se il codice in prova viene modificato, non garantisce più il contratto precedente, quindi deve fallire. E di solito non è solo un test che fallisce in situazioni come questa . In questo caso, il codice non è implementato in modo pulito. La verifica delle chiamate di metodo della dipendenza deve essere testata una sola volta (nell'apposito test unitario). Tutte le altre classi useranno solo l'istanza finta. Quindi non vedo alcun vantaggio nel mescolare l'integrazione con i test unitari.
Christopher Will,

55

Regola del pollice:

Se la funzione che stai testando necessita di un oggetto complicato come parametro, e sarebbe una semplice istanza di questo oggetto (se, ad esempio, tenta di stabilire una connessione TCP), usa un mock.


4

Dovresti prendere in giro un oggetto quando hai una dipendenza in un'unità di codice che stai provando a testare che deve essere "proprio così".

Ad esempio, quando stai provando a testare un po 'di logica nella tua unità di codice ma devi ottenere qualcosa da un altro oggetto e ciò che viene restituito da questa dipendenza potrebbe influire su ciò che stai cercando di testare - deridere quell'oggetto.

Un grande podcast sull'argomento può essere trovato qui


Il collegamento ora indirizza all'episodio corrente, non all'episodio previsto. Il podcast previsto è hanselminutes.com/32/mock-objects ?
C Perkins,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.