(Perché) è importante che un test unitario non verifichi le dipendenze?


103

Comprendo il valore dei test automatizzati e lo utilizzo ovunque il problema sia sufficientemente ben specificato da poter elaborare buoni casi di test. Ho notato, tuttavia, che alcune persone qui e su StackOverflow sottolineano il fatto di testare solo un'unità, non le sue dipendenze. Qui non riesco a vedere il vantaggio.

Deridere / stub per evitare il collaudo delle dipendenze aggiunge complessità ai test. Aggiunge requisiti di flessibilità / disaccoppiamento artificiali al codice di produzione per supportare il derisione. (Non sono d'accordo con nessuno che affermi che ciò promuove un buon design. Scrivere codice extra, introdurre cose come i framework di iniezione di dipendenza o altrimenti aggiungere complessità alla base di codice per rendere le cose più flessibili / collegabili / estensibili / disaccoppiate senza un caso d'uso reale è ingegneristico, non buon design.)

In secondo luogo, testare le dipendenze significa che il codice critico di basso livello che viene utilizzato ovunque viene testato con input diversi da quelli a cui chiunque ha scritto i test ha pensato esplicitamente. Ho trovato molti bug nella funzionalità di basso livello eseguendo test unitari su funzionalità di alto livello senza deridere la funzionalità di basso livello da cui dipendeva. Idealmente, questi sarebbero stati rilevati dai test unitari per la funzionalità di basso livello, ma si verificano sempre casi mancanti.

Qual è l'altro lato di questo? È davvero importante che un unit test non verifichi anche le sue dipendenze? Se è così, perché?

Modifica: posso capire il valore del deridere dipendenze esterne come database, reti, servizi web, ecc. (Grazie ad Anna Lear per avermi motivato a chiarire questo.) Mi riferivo a dipendenze interne , cioè altre classi, funzioni statiche, ecc. che non hanno dipendenze esterne dirette.


14
"ingegnerizzazione eccessiva, design non buono". Dovrai fornire più prove di così. Molte persone non lo chiamerebbero "ingegneria eccessiva", ma "best practice".
S.Lott

9
@ S.Lott: ovviamente questo è soggettivo. Non volevo solo un sacco di risposte a scansare il problema e dire che il deridere è buono perché rendere il tuo codice derisorio promuove un buon design. In generale, però, odio trattare con codice che è disaccoppiato in modi che non hanno evidenti benefici ora o nel prossimo futuro. Se non hai implementazioni multiple ora e non prevedi di averle nel prossimo futuro, allora IMHO dovresti semplicemente codificarlo. È più semplice e non annoia il cliente con i dettagli delle dipendenze di un oggetto.
dsimcha,

5
In generale, però, odio trattare con codice accoppiato in modi che non hanno una logica ovvia se non quella di un design scadente. È più semplice e non annoia il cliente con la flessibilità di testare le cose da solo.
S.Lott

3
@ S.Lott: Chiarimento: intendevo un codice che fa del suo meglio per disaccoppiare le cose senza un chiaro caso d'uso, specialmente dove rende il codice o il suo codice client molto più dettagliato, introduce ancora un'altra classe / interfaccia, ecc. Di Ovviamente non sto chiedendo che il codice sia accoppiato più strettamente rispetto al design più semplice e conciso. Inoltre, quando si creano prematuramente linee di astrazione, di solito finiscono nei posti sbagliati.
dsimcha,

Valuta di aggiornare la tua domanda per chiarire qualunque distinzione stai facendo. Integrare una sequenza di commenti è difficile. Si prega di aggiornare la domanda per chiarire e focalizzarla.
S.Lott

Risposte:


116

È una questione di definizione. Un test con dipendenze è un test di integrazione, non un test unitario. Dovresti anche avere una suite di test di integrazione. La differenza è che la suite di test di integrazione può essere eseguita in un diverso framework di test e probabilmente non come parte della build perché impiegano più tempo.

Per il nostro prodotto: i nostri test unitari vengono eseguiti con ogni build, impiegando pochi secondi. Un sottoinsieme dei nostri test di integrazione viene eseguito con ogni check-in, impiegando 10 minuti. La nostra suite di integrazione completa viene eseguita ogni notte, impiegando 4 ore.


4
Non dimenticare i test di regressione per coprire i bug rilevati e corretti per impedire la loro reintroduzione in un sistema durante la manutenzione futura.
RBerteig,

2
Non insisterei sulla definizione anche se concordo con essa. Alcuni codici potrebbero avere dipendenze accettabili come StringFormatter e sono ancora considerati dalla maggior parte dei test unitari.
danidacar,

2
danip: le definizioni chiare sono importanti e vorrei insistere su di esse. Ma è anche importante rendersi conto che quelle definizioni sono un obiettivo. Vale la pena puntare, ma non hai sempre bisogno di un occhio di bue. I test unitari avranno spesso dipendenze dalle librerie di livello inferiore.
Jeffrey Faust,

2
Inoltre, è importante comprendere il concetto di test di facciata, che non verifica il modo in cui i componenti si adattano insieme (come i test di integrazione) ma verifica un singolo componente in isolamento. (cioè un grafico a oggetti)
Ricardo Rodrigues,

1
non si tratta solo di essere più veloci, ma piuttosto di essere estremamente noiosi o ingestibili, immagina, se non lo fai, il tuo albero di esecuzione beffardo potrebbe crescere esponenzialmente. Immagina di deridere 10 dipendenze nella tua unità se spingi ulteriormente il beffardo diciamo che 1 di queste 10 dipendenze è usata in molti posti e ha 20 delle sue dipendenze, quindi devi deridere + 20 altre dipendenze, potenzialmente duplicatamente in molti posti. nel punto finale-api devi prendere in giro - ad esempio il database, in modo da non dover reimpostarlo ed è più veloce come hai detto
FantomX1

39

Testare con tutte le dipendenze in atto è ancora importante, ma è più nel regno dei test di integrazione come ha detto Jeffrey Faust.

Uno degli aspetti più importanti del test unitario è rendere i test affidabili. Se non ti fidi che un test di superamento significhi davvero che le cose vanno bene e che un test fallito significa davvero un problema nel codice di produzione, i tuoi test non sono così utili come possono essere.

Per rendere affidabili i tuoi test, devi fare alcune cose, ma mi concentrerò solo su uno per questa risposta. Devi assicurarti che siano facili da eseguire, in modo che tutti gli sviluppatori possano eseguirli facilmente prima di eseguire il check-in del codice. "Facile da eseguire" significa che i tuoi test vengono eseguiti rapidamente e non è necessaria una vasta configurazione o installazione per farli andare avanti. Idealmente, chiunque dovrebbe essere in grado di controllare l'ultima versione del codice, eseguire subito i test e vederli passare.

Sottrarre dipendenze da altre cose (file system, database, servizi web, ecc.) Ti consente di evitare la necessità di configurazione e rende te e altri sviluppatori meno suscettibili alle situazioni in cui sei tentato di dire "Oh, i test falliscono perché non ho impostato la condivisione di rete. Vabbè. Li eseguirò più tardi. "

Se si desidera verificare ciò che si fa con alcuni dati, i test dell'unità per quel codice di logica aziendale non dovrebbero interessarsi di come si ottengono tali dati. Essere in grado di testare la logica principale dell'applicazione senza dipendere da elementi di supporto come i database è fantastico. Se non lo fai, ti stai perdendo.

PS Dovrei aggiungere che è sicuramente possibile progettare in modo eccessivo in nome della testabilità. Il test di guida dell'applicazione aiuta a mitigarlo. Ma in ogni caso, le cattive implementazioni di un approccio non rendono l'approccio meno valido. Qualcosa può essere usato in modo improprio e esagerato se non si continua a chiedere "perché lo sto facendo?" durante lo sviluppo.


Per quanto riguarda le dipendenze interne, le cose si fanno un po 'confuse. Il modo in cui mi piace pensarci è che voglio proteggere il più possibile la mia classe dal cambiamento per motivi sbagliati. Se ho una configurazione come questa ...

public class MyClass 
{
    private SomeClass someClass;
    public MyClass()
    {
        someClass = new SomeClass();
    }

    // use someClass in some way
}

In genere non mi interessa come SomeClassviene creato. Voglio solo usarlo. Se SomeClass cambia e ora richiede parametri per il costruttore ... non è un mio problema. Non avrei dovuto cambiare MyClass per adattarlo.

Ora, questo è solo toccare la parte del design. Per quanto riguarda i test unitari, voglio anche proteggermi dalle altre classi. Se sto testando MyClass, mi piace sapere per certo che non ci sono dipendenze esterne, che SomeClass a un certo punto non ha introdotto una connessione al database o qualche altro collegamento esterno.

Ma un problema ancora più grande è che so anche che alcuni dei risultati dei miei metodi si basano sull'output di alcuni metodi su SomeClass. Senza prendere in giro / stubbing SomeClass, non avrei modo di variare quell'input richiesto. Se sono fortunato, potrei essere in grado di comporre il mio ambiente all'interno del test in modo tale da innescare la risposta corretta da SomeClass, ma andare in questo modo introduce complessità nei miei test e li rende fragili.

Riscrivere MyClass per accettare un'istanza di SomeClass nel costruttore mi consente di creare un'istanza falsa di SomeClass che restituisce il valore desiderato (tramite un framework beffardo o con una simulazione manuale). In genere non devo introdurre un'interfaccia in questo caso. Se farlo o meno è in molti modi una scelta personale che può essere dettata dalla tua lingua preferita (ad esempio le interfacce sono più probabili in C #, ma sicuramente non ne avresti bisogno in Ruby).


+1 Ottima risposta, perché le dipendenze esterne sono un caso a cui non stavo pensando quando ho scritto la domanda originale. Ho chiarito la mia domanda in risposta. Vedi l'ultima modifica.
dsimcha,

@dsimcha Ho ampliato la mia risposta per approfondire la questione. Spero che sia d'aiuto.
Adam Lear

Adam, puoi suggerire un linguaggio in cui "Se SomeClass cambia e ora richiede parametri per il costruttore ... Non dovrei cambiare MyClass" è vero? Mi dispiace, questo mi è saltato fuori come un requisito insolito. Vedo che non è necessario modificare i test unitari per MyClass, ma non dover cambiare MyClass ... wow.

1
@moz Quello che volevo dire era che se il modo in cui SomeClass è stato creato, MyClass non dovesse cambiare se SomeClass fosse iniettato in esso invece che creato internamente. In circostanze normali, MyClass non dovrebbe occuparsi dei dettagli di configurazione di SomeClass. Se l'interfaccia di SomeClass cambia, allora sì ... MyClass dovrebbe comunque essere modificato se uno qualsiasi dei metodi che utilizza è interessato.
Adam Lear

1
Se si deride SomeClass durante il test di MyClass, come si rileva se MyClass utilizza erroneamente SomeClass o si basa su una stranezza non testata di SomeClass che potrebbe cambiare?
nome

22

A parte il problema tra unità e test di integrazione, considerare quanto segue.

Il widget di classe ha dipendenze dalle classi Thingamajig e WhatsIt.

Un test unitario per Widget ha esito negativo.

In quale classe sta il problema?

Se hai risposto "avvia il debugger" o "leggi il codice fino a quando non lo trovo", capisci l'importanza di testare solo l'unità, non le dipendenze.


3
@Brook: Che ne dici di vedere quali sono i risultati per tutte le dipendenze del Widget? Se tutti passano, è un problema con Widget fino a prova contraria.
dsimcha,

4
@dsimcha, ma ora stai aggiungendo complessità al gioco per controllare i passaggi intermedi. Perché non semplificare e fare prima semplici test unitari? Quindi eseguire il test di integrazione.
asoundmove,

1
@dsimcha, sembra ragionevole finché non si entra in un grafico di dipendenza non banale. Supponiamo che tu abbia un oggetto complesso che ha 3+ strati di profondità con dipendenze, si trasforma in una ricerca O (N ^ 2) per il problema, piuttosto che in O (1)
Brook

1
Inoltre, la mia interpretazione dell'SRP è che una classe dovrebbe avere un'unica responsabilità a livello di dominio concettuale / problematico. Non importa se l'adempimento di tale responsabilità richiede che faccia un sacco di cose diverse se visto a un livello inferiore di astrazione. Se portato all'estremo, l'SRP sarebbe contrario a OO perché la maggior parte delle classi contiene dati ed esegue operazioni su di esso (due cose se visualizzate a un livello sufficientemente basso).
dsimcha,

4
@Brook: più tipi non aumentano la complessità in sé. Altri tipi vanno bene se quei tipi hanno un senso concettuale nel dominio del problema e rendono il codice più facile, non più difficile, da capire. Il problema è disaccoppiare artificialmente le cose che sono concettualmente accoppiate a livello di dominio problematico (cioè hai solo un'implementazione, probabilmente non ne avrai mai più di una, ecc.) E il disaccoppiamento non si associa bene ai concetti del dominio problematico. In questi casi è sciocco, burocratico e dettagliato creare astrazioni serie attorno a questa implementazione.
dsimcha,

14

Immagina che programmare sia come cucinare. Quindi il test unitario equivale a garantire che i tuoi ingredienti siano freschi, gustosi, ecc. Considerando che il test di integrazione è come assicurarsi che il tuo pasto sia delizioso.

In definitiva, assicurarsi che il tuo pasto sia delizioso (o che il tuo sistema funzioni) è la cosa più importante, l'obiettivo finale. Ma se i tuoi ingredienti, intendo, le unità, funzionano, avrai un modo più economico per scoprirlo.

In effetti, se puoi garantire che le tue unità / metodi funzionino, hai più probabilità di avere un sistema funzionante. Sottolineo "più probabile", al contrario di "certo". Hai ancora bisogno dei tuoi test di integrazione, proprio come hai ancora bisogno di qualcuno per assaggiare il pasto che hai cucinato e dirti che il prodotto finale è buono. Avrai un momento più facile arrivarci con ingredienti freschi, tutto qui.


7
E quando i test di integrazione falliscono, il test unitario può concentrarsi sul problema.
Tim Williscroft,

9
Per allungare un'analogia: quando scopri che il tuo pasto ha un sapore terribile, potrebbero essere necessarie alcune indagini per determinare che è perché il tuo pane è ammuffito. Il risultato, tuttavia, è che aggiungi un test unitario per verificare la presenza di pane ammuffito prima della cottura, in modo che quel particolare problema non possa più verificarsi. Quindi, se hai un altro pasto in seguito, hai eliminato il pane ammuffito come causa.
Kyralessa,

Adoro questa analogia!
thehowler,

6

Testare le unità isolandole completamente consentirà di testare TUTTE le possibili variazioni di dati e situazioni in cui questa unità può essere inviata. Poiché è isolato dal resto, è possibile ignorare le variazioni che non hanno un effetto diretto sull'unità da testare. questo a sua volta ridurrà notevolmente la complessità dei test.

I test con vari livelli di dipendenze integrati ti permetteranno di testare scenari specifici che potrebbero non essere stati testati nei test unitari.

Entrambi sono importanti. Semplicemente eseguendo il test unitario si verificherà inevitabilmente un errore sottile più complesso che si verifica quando si integrano i componenti. Fare semplicemente test di integrazione significa che stai testando un sistema senza la sicurezza che le singole parti della macchina non siano state testate. Pensare di poter ottenere un test completo migliore semplicemente eseguendo il test di integrazione è quasi impossibile poiché più componenti si aggiunge il numero di combinazioni di voci cresce MOLTO velocemente (pensa fattoriale) e la creazione di un test con una copertura sufficiente diventa molto rapidamente impossibile.

Quindi, in breve, i tre livelli di "unit test" che utilizzo in genere in quasi tutti i miei progetti:

  • Test unitari, isolati con dipendenze derise o troncate per testare l'inferno di un singolo componente. Idealmente si tenterebbe una copertura completa.
  • Test di integrazione per testare gli errori più sottili. Controlli a campione accuratamente realizzati che mostrerebbero casi limite e condizioni tipiche. La copertura completa spesso non è possibile, lo sforzo si concentra su ciò che potrebbe far fallire il sistema.
  • Test delle specifiche per iniettare vari dati di vita reale nel sistema, strumenti i dati (se possibile) e osservare l'output per assicurarsi che sia conforme alle specifiche e alle regole aziendali.

L'iniezione di dipendenza è un mezzo molto efficiente per raggiungere questo obiettivo in quanto consente di isolare i componenti per i test unitari molto facilmente senza aggiungere complessità al sistema. Il tuo scenario aziendale potrebbe non giustificare l'uso del meccanismo di iniezione, ma lo scenario del test quasi lo farà. Questo per me è sufficiente per renderlo quindi indispensabile. Puoi anche usarli per testare i diversi livelli di astrazione in modo indipendente attraverso test di integrazione parziale.


4

Qual è l'altro lato di questo? È davvero importante che un unit test non verifichi anche le sue dipendenze? Se è così, perché?

Unità. Significa singolare.

Testare 2 cose significa che hai le due cose e tutte le dipendenze funzionali.

Se aggiungi una terza cosa, aumenti le dipendenze funzionali nei test oltre linearmente. Le interconnessioni tra le cose crescono più rapidamente del numero di cose.

Esistono n (n-1) / 2 potenziali dipendenze tra n articoli in fase di test.

Questa è la grande ragione.

La semplicità ha valore.


2

Ricordi come hai imparato per la prima volta a ricorrere? Il mio prof ha detto "Supponiamo di avere un metodo che fa x" (ad esempio risolve i fibbonacci per qualsiasi x). "Per risolvere per x devi chiamare quel metodo per x-1 e x-2". Allo stesso modo, eliminare le dipendenze ti permette di fingere che esistano e testare che l'unità corrente fa quello che dovrebbe fare. Il presupposto è ovviamente che si stanno testando le dipendenze in modo rigoroso.

Questo è in sostanza l'SRP al lavoro. Concentrarsi su una singola responsabilità anche per i test, rimuove la quantità di giocoleria mentale che devi fare.


2

(Questa è una risposta minore. Grazie @TimWilliscroft per il suggerimento.)

I guasti sono più facili da localizzare se:

  • Sia l'unità sotto test e ciascuna delle sue dipendenze sono testate indipendentemente.
  • Ogni test fallisce se e solo se c'è un errore nel codice coperto dal test.

Funziona bene sulla carta. Tuttavia, come illustrato nella descrizione di OP (le dipendenze sono errate), se le dipendenze non vengono testate, sarebbe difficile individuare la posizione dell'errore.


Perché le dipendenze non dovrebbero essere testate? Sono presumibilmente di livello inferiore e più facili da testare. Inoltre, con i test unitari che coprono un codice specifico, è più facile individuare la posizione dell'errore.
David Thornley,

2

Molte buone risposte. Aggiungerei anche un paio di altri punti:

Il test unitario ti consente anche di testare il tuo codice quando non esistono dipendenze . Ad esempio, tu o il tuo team non avete ancora scritto gli altri livelli o forse state aspettando un'interfaccia fornita da un'altra società.

I test unitari significano anche che non è necessario disporre di un ambiente completo sulla propria macchina di sviluppo (ad es. Un database, un web server ecc.). Vorrei forte raccomandare che tutti gli sviluppatori che fare hanno un ambiente del genere, però abbattuto, è, al fine di bug Repro ecc Tuttavia, se per qualche motivo non è possibile simulare l'ambiente di produzione, allora il test delle unità almeno yu dà un certo livello di fiducia nel codice prima che passi a un sistema di test più grande.


0

A proposito degli aspetti di progettazione: credo che anche i piccoli progetti traggano vantaggio dal rendere testabile il codice. Non devi necessariamente introdurre qualcosa come Guice (spesso lo farà una semplice classe di fabbrica), ma separare il processo di costruzione dalla logica di programmazione porta a

  • documentare chiaramente le dipendenze di ogni classe tramite la sua interfaccia (molto utile per le persone che sono nuove nel team)
  • le classi diventano molto più chiare e più facili da mantenere (una volta che hai messo la brutta creazione del grafico a oggetti in una classe separata)
  • accoppiamento lento (che rende le modifiche molto più facili)

2
Metodo: Sì, ma devi aggiungere codice extra e classi extra per farlo. Il codice dovrebbe essere il più conciso possibile pur rimanendo leggibile. L'IMHO aggiunge fabbriche e quant'altro non è un'ingegnerizzazione eccessiva a meno che tu non possa dimostrare di aver bisogno o che molto probabilmente abbia bisogno della flessibilità che offre.
dsimcha,

0

Uh ... ci sono buoni punti su unità e test di integrazione scritti in queste risposte qui!

Mi mancano le opinioni pratiche e relative ai costi qui. Detto questo vedo chiaramente il vantaggio di test di unità molto isolati / atomici (forse altamente indipendenti l'uno dall'altro e con la possibilità di eseguirli in parallelo e senza dipendenze, come database, filesystem ecc.) E test di integrazione (di livello superiore), ma ... è anche una questione di costi (tempo, denaro, ...) e rischi .

Quindi ci sono altri fattori, che sono molto più importanti (come "cosa testare" ) prima di pensare a "come testare" dalla mia esperienza ...

Il mio cliente (implicitamente) paga per la quantità extra di scrittura e mantenimento dei test? Un approccio più guidato dai test (scrivere i test prima di scrivere il codice) è davvero conveniente nel mio ambiente (rischio di fallimento del codice / analisi dei costi, persone, specifiche di progettazione, impostare l'ambiente di test)? Il codice è sempre difettoso, ma potrebbe essere più conveniente spostare i test sull'utilizzo della produzione (nel peggiore dei casi!)?

Dipende anche molto dalla qualità del codice (standard) o dai framework, IDE, principi di progettazione ecc. Che tu e il tuo team seguite e da quanto hanno esperienza. Un codice ben scritto, facilmente comprensibile, sufficientemente documentato (idealmente auto-documentante), modulare, ... introduce probabilmente meno bug rispetto al contrario. Quindi il vero "bisogno", la pressione o i costi / rischi generali di manutenzione / correzione bug per test estesi potrebbero non essere elevati.

Portiamolo all'estremo dove un collega del mio team ha suggerito, dobbiamo provare a testare il nostro codice di livello del modello Java EE puro con una copertura del 100% desiderata per tutte le classi all'interno e deridendo il database. O il manager che vorrebbe che i test di integrazione venissero coperti con un 100% di tutti i possibili casi d'uso e flussi di lavoro dell'interfaccia utente del mondo reale perché non vogliamo rischiare di fallire qualsiasi caso d'uso. Ma abbiamo un budget limitato di circa 1 milione di euro, un piano piuttosto rigido per codificare tutto. Un ambiente per i clienti, in cui i potenziali bug delle app non costituirebbero un grande pericolo per gli esseri umani o le aziende. La nostra app verrà testata internamente con (alcuni) test unitari importanti, test di integrazione, test chiave per i clienti con piani di test progettati, una fase di test ecc. Non sviluppiamo un'app per alcune fabbriche nucleari o per la produzione farmaceutica!

Io stesso provo a scrivere test se possibile e sviluppare mentre test unitamente il mio codice. Ma lo faccio spesso con un approccio dall'alto verso il basso (test di integrazione) e cerco di trovare il punto buono, dove tagliare il "livello app" per i test importanti (spesso nel livello modello). (perché spesso dipende molto dagli "strati")

Inoltre, il codice del test unitario e di integrazione non ha un impatto negativo su tempo, denaro, manutenzione, codifica ecc. È bello e dovrebbe essere applicato, ma con cura e considerando le implicazioni quando si inizia o dopo 5 anni di molti test sviluppati codice.

Quindi direi che dipende molto dalla fiducia e dalla valutazione costi / rischi / benefici ... come nella vita reale dove non puoi e non vuoi andare in giro con meccanismi di sicurezza molto o al 100%.

Il bambino può e dovrebbe arrampicarsi da qualche parte e potrebbe cadere e ferirsi. L'auto potrebbe smettere di funzionare perché ho inserito il carburante sbagliato (input non valido :)). Il brindisi può essere bruciato se il pulsante dell'ora smette di funzionare dopo 3 anni. Ma non voglio mai guidare in autostrada e tenere il volante staccato tra le mani :)

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.