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

100

Comprendo il valore dei test automatizzati e lo utilizzo ovunque il problema sia sufficientemente specificato da consentirmi di ottenere buoni casi di test. Ho notato, tuttavia, che alcune persone qui e su StackOverflow enfatizzano il test solo di un'unità, non le sue dipendenze. Qui non riesco a vedere il beneficio.

Deridere / eseguire lo stub per evitare le dipendenze di test aggiunge complessità ai test. Aggiunge requisiti di flessibilità / disaccoppiamento artificiali al codice di produzione per supportare il mocking. (Non sono d'accordo con chi dice che questo promuove un buon design Scrittura di codice aggiuntivo, introduzione di strutture di dipendenza o aggiunta complessità alla base di codice per rendere le cose più flessibili / estensibili / estensibili / disaccoppiabili senza un vero caso d'uso è la sovrastruttura, 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 abbia scritto i test ha pensato esplicitamente. Ho trovato molti bug nella funzionalità di basso livello eseguendo test di unità su funzionalità di alto livello senza prendere in giro la funzionalità di basso livello da cui dipendeva. Idealmente questi sarebbero stati trovati dai test unitari per la funzionalità di basso livello, ma i casi mancati si verificano sempre.

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

Modifica: Riesco a capire il valore di deridere le dipendenze esterne come database, reti, servizi Web, ecc. (Grazie ad Anna Lear per avermi motivato a chiarire questo.) Mi riferivo a dipendenze interne , ovvero altre classi, funzioni statiche, ecc. che non hanno alcuna dipendenza esterna diretta.

    
posta dsimcha 06.04.2011 - 03:05
fonte

11 risposte

113

È 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 framework di test diverso e probabilmente non come parte della build perché richiedono più tempo.

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

    
risposta data 06.04.2011 - 03:11
fonte
37

Il test 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 dei test unitari è quello di rendere i test affidabili. Se non ti fidi del fatto che un test di passaggio significhi veramente che le cose sono buone e che un test non funzionante significa davvero un problema nel codice di produzione, i tuoi test non sono così utili come possono essere.

Per rendere affidabili le tue prove, devi fare alcune cose, ma mi concentrerò su una sola per questa risposta. Devi assicurarti che siano facili da eseguire, in modo che tutti gli sviluppatori possano eseguirli facilmente prima di controllare il codice. "Facile da eseguire" significa che i tuoi test scorrono rapidamente e non c'è una configurazione estesa o setup necessario per farli andare. Idealmente, chiunque dovrebbe essere in grado di controllare l'ultima versione del codice, eseguire subito i test e vederli passare.

Eliminare le dipendenze da altre cose (file system, database, servizi Web, ecc.) consente di evitare la necessità di configurazione e rende voi e altri sviluppatori meno suscettibili a situazioni in cui siete tentati di dire "Oh, i test sono falliti perché Non ho la condivisione della rete impostata, beh, li eseguirò più tardi ".

Se vuoi testare ciò che fai con alcuni dati, il tuo test di unità per quel codice di logica aziendale non dovrebbe preoccuparsi di come ottieni quei dati. Essere in grado di testare la logica di base della tua applicazione senza dipendere da materiale di supporto come i database è fantastico. Se non lo fai, ti stai perdendo.

P.S. Devo aggiungere che è sicuramente possibile effettuare interventi di supervisione in nome della testabilità. Testare la tua applicazione ti aiuta a mitigarlo. Ma in ogni caso, le cattive implementazioni di un approccio non rendono l'approccio meno valido. Qualsiasi cosa può essere usata impropriamente e esagerata se non si continua a chiedere "perché sto facendo questo?" durante lo sviluppo.

Per quanto riguarda le dipendenze interne, le cose diventano un po 'fangose. Il modo in cui mi piace pensarlo è che voglio proteggere il più possibile la mia classe dal cambiare per ragioni sbagliate. Se ho un setup come qualcosa del genere ...
public class MyClass 
{
    private SomeClass someClass;
    public MyClass()
    {
        someClass = new SomeClass();
    }

    // use someClass in some way
}

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

Ora, questo è solo toccando 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 il mocking / stubbing di SomeClass, potrei non avere modo di variare l'input della domanda. Se sono fortunato, potrei essere in grado di comporre il mio ambiente all'interno del test in modo tale da far scattare la giusta risposta da SomeClass, ma andare in questo modo introduce complessità nei miei test e renderli fragili.

Riscrivere MyClass per accettare un'istanza di SomeClass nella funzione di costruzione mi consente di creare un'istanza fittizia di SomeClass che restituisce il valore desiderato (tramite un framework di simulazione o con una simulazione manuale). In genere non ho per introdurre un'interfaccia in questo caso. Se farlo o no è per molti versi una scelta personale che può essere dettata dal linguaggio scelto (ad esempio, le interfacce sono più probabili in C #, ma sicuramente non ne avrà bisogno in Ruby).

    
risposta data 06.04.2011 - 03:21
fonte
21

A parte il problema relativo al test dell'unità di integrazione vs, prendi in considerazione quanto segue.

Class Widget ha dipendenze dalle classi Thingamajig e WhatsIt.

Un test unitario per Widget fallisce.

In quale classe si trova il problema?

Se hai risposto "accendi il debugger" o "leggi il codice finché non lo trovo", comprendi l'importanza di testare solo l'unità, non le dipendenze.

    
risposta data 06.04.2011 - 03:28
fonte
14

Immagina che la programmazione sia come cucinare. Quindi il test unitario è lo stesso per assicurarsi che i tuoi ingredienti siano freschi, gustosi, ecc. Considerando che i test di integrazione sono come assicurarsi che il tuo pasto sia delizioso.

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

In effetti, se puoi garantire il funzionamento delle tue unità / metodi, è più probabile che tu abbia 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. Ti sarà più facile arrivare con ingredienti freschi, tutto qui.

    
risposta data 06.04.2011 - 04:26
fonte
6

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

Il test con vari livelli di dipendenza integrati consentirà di testare scenari specifici che potrebbero non essere stati testati nei test unitari.

Entrambi sono importanti. Semplicemente facendo un test unitario, si verificheranno inevitabilmente degli errori più complessi che si verificano durante l'integrazione dei componenti. Fare solo test di integrazione significa testare un sistema senza la certezza che le singole parti della macchina non siano state testate. Pensare che è possibile ottenere un test completo migliore semplicemente facendo test di integrazione è quasi impossibile poiché più componenti si aggiungono il numero di combinazioni di voci cresce MOLTO veloce (si pensi fattoriali) e la creazione di un test con copertura sufficiente diventa molto rapidamente impossibile.

Quindi, in breve, i tre livelli di "test unitario" di solito li uso in quasi tutti i miei progetti:

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

L'iniezione di dipendenza è uno strumento molto efficace per ottenere ciò poiché consente di isolare i componenti per i test di unità molto facilmente senza aggiungere complessità al sistema. Lo scenario aziendale potrebbe non giustificare l'uso del meccanismo di iniezione, ma lo scenario di test quasi lo farà. Questo per me è sufficiente per rendere poi indispensabile. Puoi usarli anche per testare i diversi livelli di astrazione in modo indipendente attraverso test di integrazione parziale.

    
risposta data 06.04.2011 - 04:00
fonte
4

What's the other side to this? Is it really important that a unit test doesn't also test its dependencies? If so, why?

Unità. Significa singolare.

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

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

Ci sono n (n-1) / 2 dipendenze potenziali tra gli n elementi testati.

Questa è la grande ragione.

La semplicità ha valore.

    
risposta data 06.04.2011 - 03:10
fonte
2

Ricorda come hai imparato a fare la ricorsione? Il mio professore ha detto "Supponiamo che tu abbia un metodo che fa x" (ad esempio, risolve Fibbonacci per ogni x). "Per risolvere x devi chiamare quel metodo per x-1 e x-2". Nello stesso modo, le dipendenze di stub ti permettono di fingere che esistano e testare che l'unità corrente faccia ciò che dovrebbe fare. L'assunto è ovviamente che stai testando le dipendenze con rigore.

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

    
risposta data 06.04.2011 - 06:11
fonte
2

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

Gli errori sono più facili da localizzare se:

  • Sia l'unità sotto test sia ciascuna delle sue dipendenze vengono testate in modo indipendente.
  • Ogni test fallisce se e solo se c'è un errore nel codice coperto dal test.

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

    
risposta data 06.04.2011 - 09:16
fonte
2

Un sacco di buone risposte. Aggiungerei anche un paio di altri punti:

Il test delle unità consente anche di testare il codice quando le dipendenze non esistono . Per esempio. tu o il tuo team non avete ancora scritto gli altri livelli, o forse state aspettando un'interfaccia fornita da un'altra azienda.

I test unitari indicano inoltre che non è necessario disporre di un ambiente completo sulla macchina di sviluppo (ad esempio un database, un server Web, ecc.). Raccomanderei vivamente che tutti gli sviluppatori do abbiano un tale ambiente, per quanto ridotto, per ripro bug ecc. Tuttavia, se per qualche motivo non è possibile imitare l'ambiente di produzione, allora l'unità il test fornisce almeno un certo livello di sicurezza nel codice prima che entri in un sistema di test più ampio.

    
risposta data 06.04.2011 - 10:32
fonte
0

Informazioni sugli aspetti del design: credo che anche i piccoli progetti traggano vantaggio dal rendere il codice testabile. Non devi necessariamente introdurre qualcosa come Guice (una semplice classe di produzione spesso farà), ma separare il processo di costruzione dai risultati della logica di programmazione

  • che documenta 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 facili da gestire (una volta che hai messo la brutta creazione del grafo di oggetti in una classe separata)
  • accoppiamento lento (rendendo le modifiche molto più semplici)
risposta data 06.04.2011 - 09:45
fonte
0

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

Mi mancano le viste pratiche e relative ai costi qui. Detto questo, vedo chiaramente il beneficio di test di unità molto isolati / atomici (magari altamente indipendenti tra loro e con l'opzione 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 la quantità extra di scrittura e il mantenimento dei test? È un approccio più orientato ai test (scrivere test prima di scrivere il codice) davvero economico nel mio ambiente (rischio di errore del codice / analisi dei costi, persone, specifiche di progettazione, ambiente di test di configurazione)? Il codice è sempre buggato, ma potrebbe essere più economico spostare il test sull'utilizzo di 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 quanto sono esperti. 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 / bugfix per i test estesi potrebbero non essere elevati.

Portiamolo all'estremo in cui un collaboratore nel mio team ha suggerito, dobbiamo provare a testare unitamente il nostro codice del codice del modello Java EE puro con una copertura del 100% desiderata per tutte le classi all'interno e il mocking del database. O il manager che vorrebbe che i test di integrazione fossero coperti con il 100% di tutti i possibili casi d'uso del mondo reale e flussi di lavoro web ui perché non vogliamo rischiare il fallimento di un caso d'uso. Ma abbiamo un budget limitato di circa 1 milione di euro, un piano piuttosto stretto per codificare tutto. Un ambiente cliente, in cui i potenziali bug delle app non sarebbero un grosso pericolo per gli esseri umani o le aziende. La nostra app verrà testata internamente con (alcuni) importanti test unitari, test di integrazione, test chiave dei clienti con piani di test progettati, una fase di test, ecc. Non sviluppiamo un'app per alcune fabbriche nucleari o la produzione farmaceutica! (Abbiamo solo un database designato, facile testare il clone per ogni dev e strettamente connesso alla webapp o al livello del modello)

Io stesso provo a scrivere test se possibile e a sviluppare mentre collaudo il mio codice. Ma lo faccio spesso da un approccio top-down (test di integrazione) e cerco di trovare il punto buono, dove fare il "taglio del livello app" per i test importanti (spesso nel livello del modello). (perché spesso sono molti i "livelli")

Inoltre, il codice di test unitario e di integrazione non arriva senza impatto negativo su tempo, denaro, manutenzione, codifica, ecc. È bello e dovrebbe essere applicato, ma con cura e considerando le implicazioni all'avvio o dopo 5 anni di molto del codice di test sviluppato.

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

Il bambino può e deve arrampicarsi da qualche parte e cadere e ferirsi. L'auto potrebbe smettere di funzionare perché ho caricato il carburante sbagliato (immissione non valida :)). Il pane tostato può essere bruciato se il pulsante dell'ora ha smesso di funzionare dopo 3 anni. Ma non voglio, mai, guidare sull'autostrada e tenere tra le mani il volante distaccato:)

    
risposta data 16.01.2018 - 09:51
fonte

Leggi altre domande sui tag