Come si rilevano problemi di dipendenza con i test unitari quando si usano oggetti mock?

95

Hai una classe X e scrivi alcuni test unitari che verificano il comportamento X1. C'è anche la classe A che prende X come dipendenza.

Quando scrivi i test unitari per A, prendi in giro X. In altre parole, mentre esegui il test A dell'unità, imposti (postula) il comportamento di X's mock come X1. Il tempo passa, le persone usano il tuo sistema, ha bisogno di cambiare, X si evolve: modifichi X per mostrare il comportamento X2. Ovviamente, i test unitari per X falliranno e dovrai adattarli.

Ma con A? I test unitari per A non falliranno quando il comportamento di X viene modificato (a causa del beffardo di X). Come rilevare che il risultato di A sarà diverso quando viene eseguito con la "vera" (modificata) X?

Mi aspetto risposte del tipo: "Questo non è lo scopo del test unitario", ma quale valore ha allora il test unitario? Ti dice solo che quando passano tutti i test, non hai introdotto un cambio di rottura? E quando il comportamento di qualche classe cambia (volontariamente o involontariamente), come puoi rilevare (preferibilmente in modo automatico) tutte le conseguenze? Non dovremmo concentrarci maggiormente sui test di integrazione?

    
posta bvgheluwe 27.03.2018 - 13:53
fonte

11 risposte

125

When you write unit tests for A, you mock X

Sei tu? Io no, a meno che non sia assolutamente necessario. Devo se:

  1. X è lento o
  2. X ha effetti collaterali

Se nessuno dei due si applica, i miei test unitari di A testeranno anche X . Fare qualcos'altro sarebbe portare i test di isolamento ad un estremo illogico.

Se possiedi parti del tuo codice usando mock di altre parti del tuo codice, sarei d'accordo: qual è il punto di tali test unitari? Quindi non farlo. Lascia che quei test utilizzino le dipendenze reali in quanto formano test molto più preziosi in questo modo.

E se alcune persone si arrabbiano con te chiamando questi test, "unit test", quindi chiamali semplicemente "test automatici" e continua a scrivere buoni test automatici.

    
risposta data 27.03.2018 - 14:29
fonte
77

Hai bisogno di entrambi. Test unitari per verificare il comportamento di ciascuna unità e alcuni test di integrazione per assicurarsi che siano collegati correttamente. Il problema di affidarsi solo al test di integrazione è l'esplosione combinatoria risultante dalle interazioni tra tutte le tue unità.

Diciamo che hai la classe A, che richiede 10 test unitari per coprire completamente tutti i percorsi. Quindi hai un'altra classe B, che richiede anche 10 test unitari per coprire tutti i percorsi che il codice può intraprendere. Ora diciamo nella tua applicazione, devi alimentare l'output di A in B. Ora il tuo codice può prendere 100 diversi percorsi dall'input di A all'output di B.

Con i test unitari sono necessari solo 20 test unitari + 1 test di integrazione per coprire completamente tutti i casi.

Con i test di integrazione, avrai bisogno di 100 test per coprire tutti i percorsi del codice.

Ecco un ottimo video sugli aspetti negativi del fare affidamento solo sui test di integrazione I test integrati di JB Rainsberger sono una truffa HD

    
risposta data 27.03.2018 - 14:22
fonte
72

When you write unit tests for A, you mock X. In other words, while unit testing A, you set (postulate) the behaviour of X's mock to be X1. Time goes by, people do use your system, needs change, X evolves: you modify X to show behaviour X2. Obviously, unit tests for X will fail and you will need to adapt them.

Woah, aspetta un momento. Le implicazioni dei test per il fallimento di X sono troppo importanti per sorvolare in questo modo.

Se cambiando l'implementazione di X da X1 a X2 si interrompe il test dell'unità per X, che indica che hai apportato una modifica all'indietro al contratto X.

X2 non è una X, nel Liskov senso , quindi dovresti pensare su altri modi per soddisfare le esigenze dei tuoi stakeholder (come l'introduzione di una nuova specifica Y, che è implementata da X2).

Per approfondimenti più approfonditi, vedi Pieter Hinjens: La fine delle versioni del software o Rich Hickey Semplice semplificato .

Dal punto di vista di A, c'è una precondizione che il collaboratore rispetta il contratto X. E la tua osservazione è efficace che il test isolato per A non ti dà alcuna garanzia che A riconosca collaboratori che violano il contratto X.

Rivedi I test integrati sono una truffa ; ad alto livello, ci si aspetta che tu abbia tanti test isolati di cui hai bisogno per assicurarti che X2 implementa correttamente il contratto X, e quanti test isolati hai bisogno per assicurarti che A faccia la cosa giusta, date risposte interessanti da una X, e un numero inferiore di test integrati per garantire che X2 e A siano d'accordo su cosa significa X.

A volte vedrai questa distinzione espressa come test solitario rispetto a sociable test ; vedi Jay Fields Lavorare efficacemente con i test delle unità .

Shouldn't we focus more on integration testing?

Ancora una volta, vedere i test integrati sono una truffa - Rainsberger descrive in dettaglio un ciclo di feedback positivo che è comune (nelle sue esperienze) a progetti che fanno affidamento sui test integrati (spelling delle note). In sintesi, senza i test isolati / solitari facendo pressione sul design , la qualità degrada, portando a più errori e test più integrati ....

Avrai anche bisogno di (alcuni) test di integrazione. Oltre alla complessità introdotta da più moduli, l'esecuzione di questi test tende ad avere più resistenza rispetto ai test isolati; è più efficiente iterare su controlli molto rapidi quando il lavoro è in corso, salvando i controlli aggiuntivi per quando pensi di essere "finito".

    
risposta data 27.03.2018 - 16:01
fonte
15

Vorrei iniziare dicendo che la premessa principale della domanda è imperfetta.

Non stai mai testando (o prendendo in giro) implementazioni, stai testando (e prendendo in giro) interfacce .

Se ho una vera classe X che implementa l'interfaccia X1, posso scrivere un simulatore XM che sia conforme anche a X1. Quindi la mia classe A deve usare qualcosa che implementa X1, che può essere di classe X o mock XM.

Ora, supponiamo di cambiare X per implementare una nuova interfaccia X2. Bene, ovviamente il mio codice non è più compilato. A richiede qualcosa che implementa X1 e che non esiste più. Il problema è stato identificato e può essere risolto.

Supponiamo invece di sostituire X1, lo modifichiamo. Ora la classe A è pronta. Tuttavia, la simulazione XM non implementa più l'interfaccia X1. Il problema è stato identificato e può essere risolto.

L'intera base per il test unitario e il mocking è che si scrive codice che utilizza interfacce. A un consumatore di un'interfaccia non importa come il codice è implementato, solo che lo stesso contratto è rispettato (input / output).

Si rompe quando i tuoi metodi hanno effetti collaterali, ma penso che possa essere tranquillamente escluso perché "non può essere testato o deriso".

    
risposta data 27.03.2018 - 19:40
fonte
9

Fai le tue domande a turno:

what value does unit testing have then

Sono poco costosi da scrivere ed eseguire e ottieni un feedback tempestivo. Se rompi X, lo scoprirai più o meno immediatamente se hai dei buoni test. Non prendere in considerazione nemmeno la scrittura di test di integrazione, a meno che tu non abbia testato tutti i tuoi livelli (sì, anche sul database).

Does it really only tell you that when all tests pass, you haven't introduced a breaking change

Avere dei test che ti passano potrebbe dirti davvero poco. Potresti non aver scritto abbastanza test. Probabilmente non hai testato abbastanza scenari. La copertura del codice può aiutare qui, ma non è un proiettile d'argento. Potresti avere dei test che passano sempre . Quindi il rosso è il primo passo spesso trascurato del refactore rosso, verde.

And when some class's behaviour changes (willingly or unwillingly), how can you detect (preferably in an automated way) all the consequences

Più test - anche se gli strumenti stanno migliorando. MA dovresti definire il comportamento della classe in un'interfaccia (vedi sotto). N.B. ci sarà sempre un posto per il test manuale in cima alla piramide di test.

Shouldn't we focus more on integration testing?

Anche altri test di integrazione non sono la risposta, sono costosi da scrivere, eseguire e mantenere. A seconda della configurazione della build, il tuo build manager può escluderli comunque rendendoli dipendenti da uno sviluppatore che ricorda (mai una buona cosa!).

Ho visto gli sviluppatori passare ore a provare a correggere i test di integrazione che avrebbero trovato in cinque minuti se avessero avuto buoni test unitari. In caso contrario, prova a eseguire il software, ovvero a tutti gli utenti finali interesserà. Nessun punto con milioni di test unitari che passano se l'intero castello di carte cade quando l'utente esegue l'intera suite.

Se vuoi assicurarti che la classe A consumi la classe X nello stesso modo, dovresti usare un'interfaccia piuttosto che una concrezione. Quindi è più probabile che una variazione di rottura venga raccolta al momento della compilazione.

    
risposta data 27.03.2018 - 14:24
fonte
9

È corretto.

I test unitari sono lì per testare la funzionalità isolata di un'unità, una prima occhiata verifica che funzioni come previsto e non contenga errori stupidi.

I test unitari non sono lì per verificare che l'intera applicazione funzioni.

Ciò che molte persone dimenticano è che i test unitari sono solo i mezzi più rapidi e più sporchi per convalidare il codice. Una volta che sai che le tue piccole routine funzionano, devi eseguire anche i test di integrazione. Il test delle unità di per sé è solo marginalmente migliore di nessun test.

Il motivo per cui abbiamo test unitari è che dovrebbero essere economici. Veloce da creare ed eseguire e mantenere. Una volta che inizi a trasformarli in min test di integrazione, sei in un mondo di dolore. Potresti anche completare il test di integrazione e ignorare completamente i test di unità se hai intenzione di farlo.

ora, alcune persone pensano che un'unità non sia solo una funzione in una classe, ma l'intera classe stessa (me compreso). Tuttavia, tutto ciò fa aumentare le dimensioni dell'unità in modo da poter richiedere meno test di integrazione, ma è comunque necessario. È ancora impossibile verificare che il tuo programma faccia quello che dovrebbe fare senza una suite di test di integrazione completa.

e quindi, dovrai ancora eseguire i test di integrazione completi su un sistema live (o semi-live) per verificare che funzioni con le condizioni che il cliente utilizza.

    
risposta data 28.03.2018 - 18:34
fonte
2

I test unitari non dimostrano la correttezza di nulla. Questo è vero per tutti i test. Di solito i test unitari sono combinati con la progettazione basata su contratto (design per contratto è un altro modo di dirlo) e possibilmente prove automatiche di correttezza, se la correttezza deve essere verificata su base regolare.

Se si dispone di contratti reali, costituiti da invarianti di classe, condizioni preliminari e condizioni postali, è possibile dimostrare la correttezza gerarchica, basando la correttezza dei componenti di livello superiore sui contratti di componenti di livello inferiore. Questo è il concetto fondamentale dietro design by contract.

    
risposta data 27.03.2018 - 20:33
fonte
2

Trovo che test pesantemente derisi siano raramente utili. Il più delle volte, finisco per reimplementare un comportamento che la classe originale ha già, che sconfigge totalmente lo scopo di deridere.

M.e. una strategia migliore è quella di avere una buona separazione delle preoccupazioni (ad es. puoi testare la parte A della tua app senza introdurre le parti dalla B alla Z). Una così buona architettura aiuta davvero a scrivere un buon test.

Inoltre, sono più che disposto ad accettare gli effetti collaterali fintanto che posso riportarli indietro, ad es. se il mio metodo modifica i dati nel db, fallo! Finché posso riportare il db allo stato precedente, qual è il danno? Inoltre, c'è il vantaggio che il mio test può verificare se i dati appaiono come previsto. I DB in memoria o le versioni di test specifiche di dbs aiutano molto qui (ad esempio la versione di prova in memoria di RavenDBs).

Infine, mi piace fare il mocking sui limiti del servizio, ad es. non effettuare la chiamata http al servizio b, ma intercettiamola e introduciamo un appropriato

    
risposta data 31.03.2018 - 07:19
fonte
1

Vorrei che le persone in entrambi i campi capissero che i test di classe e il test comportamentale non sono ortogonali.

I test di classe e le unit test sono usati in modo intercambiabile e forse non dovrebbero esserlo. Alcuni test di unità sono stati implementati in classi. Questo è tutto. Il test delle unità è accaduto per decenni nelle lingue senza classi.

Per quanto riguarda i comportamenti di test, è perfettamente possibile farlo nei test di classe usando il costrutto GWT.

Inoltre, se i tuoi test automatici procedono lungo le linee di classe o di comportamento, dipende piuttosto dalle tue priorità. Alcuni dovranno rapidamente prototipare e ottenere qualcosa fuori dalla porta, mentre altri avranno vincoli di copertura a causa di stili interni. Molte ragioni. Sono entrambi approcci perfettamente validi. Paghi i tuoi soldi, fai la tua scelta.

Quindi, cosa fare quando il codice si rompe. Se è stato codificato su un'interfaccia, solo la concrezione deve cambiare (insieme a qualsiasi test).

Tuttavia, l'introduzione di un nuovo comportamento non deve assolutamente compromettere il sistema. Linux e altri sono pieni di funzionalità deprecate. E cose come costruttori (e metodi) possono essere sovraccaricate senza forzare tutto il codice chiamante a cambiare.

Dove vince il test di classe è dove devi apportare una modifica a una classe che non è stata ancora scandita (a causa di vincoli di tempo, complessità o altro). È molto più facile iniziare una lezione se ha test completi.

    
risposta data 28.03.2018 - 19:42
fonte
0

A meno che l'interfaccia per X non sia cambiata, non è necessario cambiare il test di unità per A perché nulla di correlato ad A è cambiato. Sembra che tu abbia davvero scritto un test unitario di X e A insieme, ma lo hai chiamato un test unitario di A:

When you write unit tests for A, you mock X. In other words, while unit testing A, you set (postulate) the behaviour of X's mock to be X1.

Idealmente, la simulazione di X dovrebbe simulare tutti i possibili comportamenti di X, non solo il comportamento che ti aspetti da X. Quindi non importa ciò che implementa effettivamente in X, A dovrebbe già essere in grado di gestirlo. Quindi nessuna modifica a X, oltre a cambiare l'interfaccia stessa, avrà alcun effetto sul test dell'unità per A.

Ad esempio: Supponiamo che A sia un algoritmo di ordinamento e A fornisce dati da ordinare. La simulazione di X dovrebbe fornire un valore di ritorno nullo, una lista vuota di dati, un singolo elemento, più elementi già ordinati, più elementi non ancora ordinati, più elementi ordinati all'indietro, elenchi con lo stesso elemento ripetuto, valori nulli mescolati, ridicolmente un numero elevato di elementi e dovrebbe anche generare un'eccezione.

Quindi forse X inizialmente restituiva i dati ordinati il lunedì e le liste vuote il martedì. Ma ora quando X restituisce dati non ordinati il lunedì e lancia eccezioni il martedì, A non si preoccupa - quegli scenari erano già coperti nel test unitario di A.

    
risposta data 28.03.2018 - 16:50
fonte
-2

Devi guardare diversi test.

I test unitari eseguiranno solo il test di X. Sono lì per evitare di modificare il comportamento di X ma non proteggere l'intero sistema. Si assicurano di poter refactoring classe senza introdurre un cambiamento nel comportamento. E se rompi X, lo hai rotto ...

A dovrebbe effettivamente prendere in giro X per i suoi test unitari e il test con il Mock dovrebbe continuare a passare anche dopo averlo modificato.

Ma c'è più di un livello di test! Ci sono anche test di integrazione. Questi test servono a convalidare l'interazione tra le classi. Questi test di solito hanno un prezzo più alto dal momento che non usano mock per tutto. Ad esempio, un test di integrazione potrebbe effettivamente scrivere un record in un database e un test unitario non dovrebbe avere dipendenze esterne.

Anche se X ha bisogno di avere un nuovo comportamento, sarebbe meglio fornire un nuovo metodo che fornisca l'esito desiderato

    
risposta data 29.03.2018 - 10:08
fonte

Leggi altre domande sui tag