Domanda

Ho una conoscenza di base degli oggetti finti e falsi, ma non sono sicuro di avere un'idea di quando/dove utilizzare il mocking, soprattutto perché si applicherebbe a questo scenario Qui.

È stato utile?

Soluzione

Un test unitario dovrebbe testare un singolo percorso di codice attraverso un singolo metodo.Quando l'esecuzione di un metodo passa all'esterno di quel metodo, in un altro oggetto e viceversa, si ha una dipendenza.

Quando esegui il test del percorso del codice con la dipendenza effettiva, non stai effettuando un test unitario;stai testando l'integrazione.Sebbene sia positivo e necessario, non si tratta di test unitari.

Se la tua dipendenza è difettosa, il tuo test potrebbe essere influenzato in modo tale da restituire un falso positivo.Ad esempio, potresti passare alla dipendenza un null imprevisto e la dipendenza potrebbe non generare null come è documentato.Il test non riscontra un'eccezione di argomento nullo come dovrebbe e il test viene superato.

Inoltre, potresti trovare difficile, se non impossibile, fare in modo che l'oggetto dipendente restituisca esattamente ciò che desideri durante un test.Ciò include anche la generazione di eccezioni previste all'interno dei test.

Una simulazione sostituisce quella dipendenza.Imposta le aspettative sulle chiamate all'oggetto dipendente, imposta gli esatti valori restituiti che dovrebbe fornirti per eseguire il test che desideri e/o quali eccezioni generare in modo da poter testare il codice di gestione delle eccezioni.In questo modo potrete testare facilmente l'unità in questione.

TL;DR:Prendi in giro ogni dipendenza toccata dal tuo test unitario.

Altri suggerimenti

Gli oggetti finti sono utili quando vuoi testare le interazioni tra una classe sotto test e una particolare interfaccia.

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

Con oggetti finti, invece di passarne uno reale MailServerImpl, o un test TestMailServer, possiamo passare un'implementazione simulata di MailServer interfaccia.Prima di passare una finta MailServer, lo "addestriamo", in modo che sappia quali chiamate di metodo aspettarsi e quali valori restituire.Alla fine, l'oggetto fittizio asserisce che tutti i metodi previsti sono stati chiamati come previsto.

In teoria questo suona bene, ma ci sono anche alcuni aspetti negativi.

Finte carenze

Se disponi di un framework fittizio, sei tentato di utilizzare un oggetto mock ogni volta è necessario passare un'interfaccia alla classe sottoposta al test.In questo modo finisci testare le interazioni anche quando non è necessario.Sfortunatamente, il test indesiderato (accidentale) delle interazioni è negativo, perché in questo modo stai testando che un particolare requisito è implementato in un modo particolare, invece di verificare che l'implementazione abbia prodotto il risultato richiesto.

Ecco un esempio in pseudocodice.Supponiamo di aver creato un file MySorter class e vogliamo testarlo:

// 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 assumiamo che non si voglia testare un particolare algoritmo di ordinamento, come l'ordinamento rapido;in tal caso, quest’ultimo test sarebbe effettivamente valido.)

In un esempio così estremo è ovvio il motivo per cui quest'ultimo esempio è sbagliato.Quando modifichiamo l'implementazione di MySorter, il primo test fa un ottimo lavoro assicurandosi che venga comunque ordinato correttamente, che è il punto centrale dei test: ci consentono di modificare il codice in modo sicuro.D'altra parte, quest'ultimo test Sempre si rompe ed è attivamente dannoso;ostacola il refactoring.

Mock come stub

I framework simulati spesso consentono anche un utilizzo meno rigido, dove non dobbiamo specificare esattamente quante volte i metodi dovrebbero essere chiamati e quali parametri sono attesi;consentono di creare oggetti finti che vengono utilizzati come stub.

Supponiamo di avere un metodo sendInvitations(PdfFormatter pdfFormatter, MailServer mailServer) che vogliamo testare.IL PdfFormatter l'oggetto può essere utilizzato per creare l'invito.Ecco la prova:

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 il PdfFormatter object quindi lo addestriamo semplicemente ad accettare tranquillamente qualsiasi chiamata e restituire alcuni valori di ritorno predefiniti sensati per tutti i metodi che sendInvitation() capita di chiamare a questo punto.Come siamo arrivati ​​a stilare esattamente questo elenco di metodi per allenarci?Abbiamo semplicemente eseguito il test e continuato ad aggiungere metodi finché il test non è stato superato.Nota che abbiamo addestrato lo stub a rispondere a un metodo senza avere la minima idea del motivo per cui deve chiamarlo, abbiamo semplicemente aggiunto tutto ciò di cui il test si lamentava.Siamo contenti, la prova è superata.

Ma cosa succede dopo, quando cambiamo sendInvitations(), o qualche altra classe quella sendInvitations() utilizza, per creare PDF più fantasiosi?Il nostro test fallisce improvvisamente perché ora ci sono più metodi di PdfFormatter vengono chiamati e non abbiamo addestrato il nostro stub ad aspettarli.E di solito non è solo un test a fallire in situazioni come questa, ma qualsiasi test che utilizza, direttamente o indirettamente, il sendInvitations() metodo.Dobbiamo correggere tutti questi test aggiungendo ulteriori corsi di formazione.Si noti inoltre che non è possibile rimuovere i metodi non più necessari perché non sappiamo quali di essi non siano necessari.Ancora una volta, ostacola il refactoring.

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

Come risolverlo?Facilmente:

  • Prova a utilizzare classi reali invece di simulazioni quando possibile.Usa il reale PdfFormatterImpl.Se non è possibile, cambia le classi reali per renderlo possibile.Non poter utilizzare una classe nei test solitamente indica alcuni problemi con la classe.Risolvere i problemi è una situazione vantaggiosa per tutti: hai risolto la lezione e hai un test più semplice.D'altra parte, non risolverlo e utilizzare mock è una situazione senza via d'uscita: non hai corretto la classe reale e hai test più complessi e meno leggibili che ostacolano ulteriori refactoring.
  • Prova a creare una semplice implementazione di test dell'interfaccia invece di simularla in ogni test e utilizza questa classe di test in tutti i tuoi test.Creare TestPdfFormatter questo non fa nulla.In questo modo puoi cambiarlo una volta per tutti i test e i tuoi test non saranno ingombrati da lunghe configurazioni in cui alleni i tuoi stub.

Tutto sommato, gli oggetti finti hanno la loro utilità, ma se non usati con attenzione, spesso incoraggiano cattive pratiche, testando i dettagli di implementazione, ostacolano il refactoring e producono test difficili da leggere e da mantenere.

Per ulteriori dettagli sulle carenze dei mock vedere anche Oggetti finti:Carenze e casi d'uso.

Regola del pollice:

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

Dovresti deridere un oggetto quando hai una dipendenza in un'unità di codice che stai cercando di testare che deve essere "proprio così".

Ad esempio, quando stai provando a testare una logica nella tua unità di codice ma devi ottenere qualcosa da un altro oggetto e ciò che viene restituito da questa dipendenza potrebbe influenzare ciò che stai cercando di testare, prendi in giro quell'oggetto.

È possibile trovare un ottimo podcast sull'argomento Qui

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top