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 .
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 .
Risposte:
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.
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.
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.
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:
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.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 .
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.
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