Quando utilizzare Mockito.verify ()?


201

Scrivo casi di test jUnit per 3 scopi:

  1. Per garantire che il mio codice soddisfi tutte le funzionalità richieste, sotto tutti (o la maggior parte) delle combinazioni / valori di input.
  2. Per essere sicuro di poter cambiare l'implementazione e fare affidamento sui casi di test JUnit per dirmi che tutte le mie funzionalità sono ancora soddisfatte.
  3. Come documentazione di tutti i casi d'uso gestiti dal mio codice e fungere da specifica per il refactoring, nel caso in cui il codice debba mai essere riscritto. (Rifattorizzare il codice e se i miei test jUnit falliscono, probabilmente hai perso un caso d'uso).

Non capisco perché o quando Mockito.verify()dovrebbe essere usato. Quando vedo di verify()essere chiamato, mi sta dicendo che la mia jUnit sta diventando consapevole dell'implementazione. (Pertanto, la modifica della mia implementazione romperebbe le mie jUnit, anche se la mia funzionalità non era interessata).

Sto cercando:

  1. Quali dovrebbero essere le linee guida per un uso appropriato di Mockito.verify() ?

  2. È fondamentalmente corretto che jUnits sia a conoscenza o strettamente associato all'implementazione della classe sottoposta a test?


1
Cerco di stare lontano dall'uso diify () il più possibile, per lo stesso motivo per cui hai esposto (non voglio che il mio test unit venga a conoscenza dell'implementazione), ma c'è un caso in cui non ho scelta - metodi vuoti stub. In generale, poiché non restituiscono nulla, non contribuiscono al tuo output "effettivo"; ma comunque, devi sapere che è stato chiamato. Ma sono d'accordo con te che non ha senso utilizzare la verifica per verificare il flusso di esecuzione.
Legna,

Risposte:


78

Se il contratto di classe A include il fatto che chiama il metodo B di un oggetto di tipo C, è necessario verificarlo eseguendo una simulazione di tipo C e verificando che sia stato chiamato il metodo B.

Ciò implica che il contratto di classe A ha dettagli sufficienti per parlare di tipo C (che potrebbe essere un'interfaccia o una classe). Quindi sì, stiamo parlando di un livello di specifica che va oltre i "requisiti di sistema" e che descrive in qualche modo l'implementazione.

Questo è normale per i test unitari. Quando stai testando l'unità, vuoi assicurarti che ogni unità stia facendo la "cosa giusta" e che di solito includerà le sue interazioni con altre unità. "Unità" qui può significare classi o sottoinsiemi più grandi dell'applicazione.

Aggiornare:

Ritengo che ciò non si applichi solo alla verifica, ma anche al mobbing. Non appena si interrompe un metodo di una classe collaboratrice, il test unitario è diventato, in un certo senso, dipendente dall'implementazione. È un po 'nella natura dei test unitari essere così. Dato che Mockito riguarda sia la mobbing che la verifica, il fatto che tu stia usando Mockito implica che ti imbatterai in questo tipo di dipendenza.

Nella mia esperienza, se cambio l'implementazione di una classe, spesso devo cambiare l'implementazione dei suoi test unitari affinché corrispondano. In genere, tuttavia, non dovrò cambiare l'inventario di quali test unitari ci sono per la classe; a meno che, naturalmente, la ragione del cambiamento sia stata l'esistenza di una condizione che non avevo verificato prima.

Quindi questo è ciò che riguarda i test unitari. Un test che non soffre di questo tipo di dipendenza dal modo in cui vengono utilizzate le classi dei collaboratori è in realtà un test del sottosistema o un test di integrazione. Naturalmente, questi sono spesso scritti anche con JUnit e spesso implicano l'uso del derisione. Secondo me, "JUnit" è un nome terribile, per un prodotto che ci consente di produrre tutti i diversi tipi di test.


8
Grazie David. Dopo aver scansionato alcuni set di codici, questa sembra una pratica comune, ma per me ciò vanifica lo scopo di creare unit test e aggiunge semplicemente il sovraccarico di mantenerli per un valore molto basso. Capisco perché sono necessari i mock e perché le dipendenze per l'esecuzione del test devono essere configurate. Ma verificare che la dipendenza metodo A.XYZ () sia eseguita rende i test molto fragili, secondo me.
Russell,

@Russell Anche se "tipo C" è un'interfaccia per un wrapper attorno a una libreria o attorno a un sottosistema distinto dell'applicazione?
Dawood ibn Kareem,

1
Non direi che è completamente inutile garantire che sia stato invocato qualche sottosistema o servizio, solo che dovrebbero esserci delle linee guida intorno (formularli era ciò che volevo fare). Ad esempio: (probabilmente sto semplicemente semplificando) Dire, sto usando StrUtil.equals () nel mio codice e decido di passare a StrUtil.equalsIgnoreCase () nell'implementazione. Se jUnit avesse verificato (StrUtil.equals ), il mio test potrebbe non riuscire sebbene l'implementazione sia accurata. Questa chiamata di verifica, IMO, è una cattiva pratica anche se è per librerie / sottosistemi. D'altra parte, l'utilizzo di verifica per garantire una chiamata a closeDbConn potrebbe essere un caso d'uso valido.
Russell,

1
Ti capisco e sono completamente d'accordo con te. Ma sento anche che scrivere le linee guida che descrivi potrebbe espandersi nella scrittura di un intero libro di testo TDD o BDD. Per fare un esempio, chiamare equals()o equalsIgnoreCase()non sarebbe mai qualcosa che è stato specificato nei requisiti di una classe, quindi non avrebbe mai un unit test di per sé. Tuttavia, "chiudere la connessione DB una volta terminato" (qualunque cosa ciò significhi in termini di implementazione) potrebbe benissimo essere un requisito di una classe, anche se non è un "requisito aziendale". Per me, questo dipende dal rapporto tra il contratto ...
Dawood ibn Kareem,

... di una classe espressa nei suoi requisiti aziendali e l'insieme dei metodi di prova che l'unità verifica quella classe. Definire questa relazione sarebbe un argomento importante in qualsiasi libro su TDD o BDD. Mentre qualcuno nel team di Mockito potrebbe scrivere un post su questo argomento per la sua wiki, non vedo in che modo differirebbe da molta altra letteratura disponibile. Se vedi come potrebbe differire, fammi sapere e forse possiamo lavorarci insieme.
Dawood ibn Kareem,

60

La risposta di David è ovviamente corretta, ma non spiega del tutto perché lo vorresti.

Fondamentalmente, quando si esegue il test di unità, si verifica un'unità di funzionalità in isolamento. Si verifica se l'input produce l'output previsto. A volte, devi testare anche gli effetti collaterali. In poche parole, verifica ti consente di farlo.

Ad esempio, hai un po 'di logica aziendale che dovrebbe archiviare le cose usando un DAO. È possibile farlo utilizzando un test di integrazione che crea un'istanza del DAO, lo collega alla logica aziendale e quindi si sposta nel database per vedere se le cose previste sono state archiviate. Non è più un test unitario.

In alternativa, è possibile deridere il DAO e verificare che venga chiamato nel modo previsto. Con mockito puoi verificare che venga chiamato qualcosa, quanto spesso viene chiamato e persino utilizzare i corrispondenti sui parametri per assicurarti che venga chiamato in un modo particolare.

Il rovescio della medaglia di test di unità come questo è infatti che si stanno collegando i test all'implementazione, il che rende un po 'più difficile il refactoring. D'altra parte, un buon odore di design è la quantità di codice necessaria per esercitarlo correttamente. Se i tuoi test devono essere molto lunghi, probabilmente qualcosa non va nel design. Quindi il codice con molti effetti collaterali / interazioni complesse che devono essere testate non è probabilmente una buona cosa da avere.


30

Questa è un'ottima domanda! Penso che la causa principale sia la seguente, stiamo usando JUnit non solo per test unitari. Quindi la domanda dovrebbe essere divisa:

  • Dovrei usare Mockito.verify () nei miei test di integrazione (o qualsiasi altro test superiore all'unità)?
  • Dovrei usare Mockito.verify () nel mio test unitario della scatola nera ?
  • Dovrei usare Mockito.verify () nel mio test unitario in scatola bianca ?

quindi se ignoreremo i test superiori alle unità, la domanda può essere riformulata "L' uso del test unitario in white box con Mockito.verify () crea una grande coppia tra i test unitari e la mia possibile implementazione, posso creare un " riquadro grigio " unit test e quali regole empiriche dovrei usare per questo ".

Ora esaminiamo tutto questo passo per passo.

* - Dovrei usare Mockito.verify () nella mia integrazione test di (o qualsiasi altro test superiore all'unità)? * Penso che la risposta sia chiaramente no, inoltre non dovresti usare mock per questo. Il test dovrebbe essere il più vicino possibile all'applicazione reale. Stai testando il caso d'uso completo, non parte isolata dell'applicazione.

* test di unità black-box vs white-box * Se stai usando l' approccio black-box su cosa stai realmente facendo, fornisci input (tutte le classi di equivalenza), uno stato e test che riceverai l'output previsto. In questo approccio l'uso delle beffe in generale è giustificato (semplicemente imiti che stanno facendo la cosa giusta; non vuoi metterle alla prova), ma chiamare Mockito.verify () è superfluo.

Se stai usando l' approccio white-box che cosa stai realmente facendo, stai testando il comportamento della tua unità. In questo approccio è essenziale chiamare Mockito.verify (), è necessario verificare che l'unità si comporti come previsto.

regole del pollice per il test della scatola grigia Il problema con il test della scatola bianca è che crea un accoppiamento elevato. Una possibile soluzione è quella di eseguire test su scatola grigia, non su scatola bianca. Questa è una sorta di combinazione di test in bianco e nero. Stai davvero testando il comportamento della tua unità come nei test white-box, ma in generale la rendi indipendente dall'implementazione quando possibile . Quando è possibile, effettuerai un controllo come nel caso di una scatola nera, asserendo solo che l'output è quello che ti aspetti. Quindi, l'essenza della tua domanda è quando è possibile.

Questo è davvero difficile. Non ho un buon esempio, ma posso darti degli esempi. Nel caso che è stato menzionato sopra con equals () vs equalsIgnoreCase () non dovresti chiamare Mockito.verify (), basta affermare l'output. Se non è possibile farlo, suddividere il codice nell'unità più piccola, fino a quando non è possibile farlo. D'altra parte, supponi di avere un po 'di @Service e stai scrivendo @ Web-Service che è essenzialmente un wrapper sul tuo @Service - delega tutte le chiamate al @Service (e sta facendo qualche gestione extra degli errori). In questo caso è essenziale chiamare Mockito.verify (), non è necessario duplicare tutti i controlli effettuati per @Serive, verificando che sia sufficiente chiamare @Service con l'elenco di parametri corretto.


Il test delle scatole grigie è un po 'una trappola. Tendo a limitarlo a cose come i DAO. Ho partecipato ad alcuni progetti con build estremamente lente a causa dell'abbondanza di test di scatole grigie, di una mancanza quasi completa di test unitari e di troppi test di blackbox per compensare la mancanza di fiducia in ciò che i test di greybox stavano presumibilmente testando.
Jilles van Gurp,

Per me questa è la migliore risposta disponibile poiché risponde quando usare Mockito.when () in una varietà di situazioni. Molto bene.
Michiel Leegwater,

8

Devo dire che hai assolutamente ragione dal punto di vista dell'approccio classico:

  • Se prima crei (o modifichi) la logica di business della tua applicazione e poi la copri con (adotti) test ( approccio Test-Last ), sarà molto doloroso e pericoloso far sapere ai test qualcosa su come funziona il tuo software, diverso da controllo degli ingressi e delle uscite.
  • Se stai esercitando un approccio Test-Driven , i tuoi test sono i primi a essere scritti, modificati e che riflettono i casi d'uso della funzionalità del tuo software. L'implementazione dipende dai test. Questo a volte significa che vuoi che il tuo software sia implementato in qualche modo particolare, ad esempio affidati al metodo di qualche altro componente o addirittura chiamalo un determinato numero di volte. È qui che Mockito.verify () torna utile!

È importante ricordare che non esistono strumenti universali. Il tipo di software, le sue dimensioni, gli obiettivi dell'azienda e la situazione del mercato, le capacità del team e molte altre cose influenzano la decisione su quale approccio utilizzare nel tuo caso particolare.


0

Come alcune persone hanno detto

  1. A volte non hai un output diretto sul quale puoi affermare
  2. A volte devi solo confermare che il tuo metodo testato sta inviando i risultati indiretti corretti ai suoi collaboratori (che stai deridendo).

Per quanto riguarda la tua preoccupazione riguardo al superamento dei test durante il refactoring, questo è in qualche modo previsto quando si usano beffe / tronconi / spie. Voglio dire che per definizione e non per quanto riguarda un'implementazione specifica come Mockito. Ma si potrebbe pensare in questo modo - se hai bisogno di fare un refactoring che creerebbe grandi cambiamenti sul modo in cui il metodo funziona, è una buona idea per farlo su un approccio TDD, significa che è possibile cambiare il test prima di definire il nuovo comportamento (che non supererà il test), quindi eseguirà le modifiche e supererà nuovamente il test.


0

Nella maggior parte dei casi, quando alle persone non piace usare Mockito.verify, è perché viene utilizzato per verificare tutto ciò che l'unità testata sta facendo e ciò significa che sarà necessario adattare il test se qualcosa cambia. Ma non penso che sia un problema. Se vuoi essere in grado di cambiare ciò che fa un metodo senza la necessità di cambiarne il test, questo significa fondamentalmente che vuoi scrivere test che non testano tutto ciò che il tuo metodo sta facendo, perché non vuoi che provi le tue modifiche . E questo è il modo di pensare sbagliato.

Ciò che è veramente un problema è che puoi modificare ciò che fa il tuo metodo e un test unitario che dovrebbe coprire interamente la funzionalità non fallisce. Ciò significherebbe che qualunque sia l'intenzione del tuo cambiamento, il risultato del tuo cambiamento non è coperto dal test.

Per questo motivo, preferisco deridere il più possibile: anche deridere i tuoi oggetti dati. Nel fare ciò, è possibile non solo verificare per verificare che vengano chiamati i metodi corretti di altre classi, ma anche che i dati trasmessi vengano raccolti tramite i metodi corretti di tali oggetti dati. E per completarlo, è necessario testare l'ordine in cui si verificano le chiamate. Esempio: se si modifica un oggetto entità db e lo si salva utilizzando un repository, non è sufficiente verificare che i setter dell'oggetto siano chiamati con i dati corretti e che sia chiamato il metodo save del repository. Se vengono chiamati nell'ordine sbagliato, il tuo metodo non fa ancora quello che dovrebbe fare. Quindi, non uso Mockito.verify ma creo un oggetto inOrder con tutti i mock e uso invece inOrder.verify. E se vuoi completarlo, dovresti anche chiamare Mockito. verificaNoMoreInteractions alla fine e passa tutte le beffe. Altrimenti qualcuno può aggiungere nuove funzionalità / comportamenti senza testarlo, il che significherebbe dopo che le tue statistiche di copertura possono essere al 100% e stai ancora accumulando codice che non viene affermato o verificato.

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.