I test di integrazione devono ripetere tutti i test unitari?


37

Diciamo che ho una funzione (scritta in Ruby, ma dovrebbe essere comprensibile a tutti):

def am_I_old_enough?(name = 'filip')
   person = Person::API.new(name)
   if person.male?
      return person.age > 21
   else
      return person.age > 18
   end
end

Nel test unitario creerei quattro test per coprire tutti gli scenari. Ognuno utilizzerà Person::APIoggetti derisi con metodi stub male?e age.

Ora si tratta di scrivere test di integrazione. Presumo che Person :: API non debba più essere deriso. Quindi vorrei creare esattamente gli stessi quattro casi di test, ma senza deridere l'oggetto Person :: API. È corretto?

Se sì, allora a che serve scrivere unit test, se solo potessi scrivere test di integrazione che mi diano maggiore sicurezza (mentre lavoro su oggetti reali, non stub o beffe)?


3
Bene, uno dei punti è che deridendo / testando l'unità, è possibile isolare qualsiasi problema al codice. Se un test di integrazione ha esito negativo, non si è consapevoli del codice non funzionante, del proprio o dell'API.
Chris Wohlert,

9
Solo quattro prove? Hai sei età limite che dovresti testare: 17, 18, 19, 20, 21, 22 ...;)
David Arno

22
@FilipBartuzi, suppongo che il metodo stia verificando se un maschio ha più di 21 anni, per esempio? Come attualmente scritto, non lo fa, è vero solo se hanno più di 22 anni. "Over 21" in inglese significa "21+". Quindi c'è un bug nel tuo codice. Tali bug vengono rilevati testando i valori limite, ovvero 20, 21, 22 per un maschio, 17,18,19 per una femmina in questo caso. Quindi sono necessari almeno sei test.
David Arno,

6
Per non parlare dei casi di 0 e -1. Cosa significa che una persona ha -1 anni? Cosa dovrebbe fare il codice se l'API restituisce qualcosa di privo di senso?
RubberDuck,

9
Sarebbe molto più semplice testare se si passasse un oggetto persona come parametro.
JeffO,

Risposte:


72

No, i test di integrazione non dovrebbero semplicemente duplicare la copertura dei test unitari. Essi possono duplicare qualche copertura, ma non è questo il punto.

Il punto di un test unitario è garantire che un piccolo bit specifico di funzionalità funzioni esattamente e completamente come previsto. Un test unitario verificherebbe am_i_old_enoughi dati con età diverse, sicuramente quelle vicine alla soglia, possibilmente tutte verificatesi in età umana. Dopo aver scritto questo test, l'integrità di am_i_old_enoughnon dovrebbe mai più essere messa in discussione.

Il punto di un test di integrazione è verificare che l'intero sistema, o una combinazione di un numero considerevole di componenti, faccia la cosa giusta se usati insieme . Il cliente non si preoccupa di una particolare funzione di utilità che hai scritto, si preoccupa che la sua app web sia adeguatamente protetta contro l'accesso dei minori, perché altrimenti i regolatori avranno i loro culi.

Il controllo dell'età dell'utente è una piccola parte di tale funzionalità, ma il test di integrazione non verifica se la funzione di utilità utilizza il valore di soglia corretto. Verifica se il chiamante prende la decisione giusta in base a tale soglia, se viene chiamata la funzione di utilità, se sono soddisfatte altre condizioni di accesso, ecc.

La ragione per cui abbiamo bisogno di entrambi i tipi di test è fondamentalmente che c'è un'esplosione combinatoria di possibili scenari per il percorso attraverso una base di codice che l'esecuzione può richiedere. Se la funzione di utilità ha circa 100 possibili input e ci sono centinaia di funzioni di utilità, quindi verificare che la cosa giusta accada in tutti i casi richiederebbe molti, molti milioni di casi di test. Semplicemente controllando tutti i casi in ambiti molto piccoli e quindi controllando combinazioni comuni, pertinenti o probabili per questi ambiti, assumendo che questi piccoli ambiti siano già corretti, come dimostrato dal test unitario , possiamo ottenere una valutazione abbastanza sicura che il sistema stia facendo cosa dovrebbe, senza annegare in scenari alternativi da testare.


6
"possiamo ottenere una valutazione abbastanza sicura che il sistema stia facendo quello che dovrebbe, senza annegare in scenari alternativi da testare". Grazie. Adoro quando qualcuno si avvicina ai test automatizzati con sanità mentale.
jpmc26,

1
JB Rainsberger ha un bel discorso sui test e sull'esplosione combinatoria di cui stai scrivendo nell'ultimo paragrafo, chiamato "I test integrati sono una truffa" . Non si tratta molto dei test di integrazione, ma è comunque piuttosto interessante.
Bart van Nierop,

The customer doesn't care about a particular utility function you wrote, they care that their web app is properly secured against access by minors-> Questa è una mentalità molto intelligente, grazie! Il problema è quando progetti per te stesso. È difficile dividere la tua mentalità tra l'essere un programmatore e l'essere un product manager nello stesso momento
Filip Bartuzi,

14

La risposta breve è "No". La parte più interessante è perché / come questa situazione potrebbe sorgere.

Penso che la confusione stia sorgendo perché stai cercando di aderire a rigorose pratiche di test (unit test vs test di integrazione, derisione, ecc.) Per il codice che non sembra aderire a pratiche rigorose.

Questo non vuol dire che il codice sia "sbagliato" o che determinate pratiche siano migliori di altre. Semplicemente che alcune delle ipotesi formulate dalle pratiche di sperimentazione potrebbero non applicarsi in questa situazione e potrebbe aiutare a usare un livello simile di "rigore" nelle pratiche di codifica e nelle pratiche di sperimentazione; o almeno, riconoscere che potrebbero essere sbilanciati, il che renderà alcuni aspetti inapplicabili o ridondanti.

Il motivo più ovvio è che la tua funzione sta eseguendo due diversi compiti:

  • Cercare un in Personbase al loro nome. Ciò richiede test di integrazione, per assicurarsi che possano trovare Personoggetti che presumibilmente vengono creati / memorizzati altrove.
  • Calcolare se a Personè abbastanza vecchio, in base al loro genere. Ciò richiede un test unitario, per assicurarsi che il calcolo funzioni come previsto.

Raggruppando queste attività in un blocco di codice, non è possibile eseguirne una senza l'altra. Quando si desidera testare l'unità dei calcoli, si è costretti a cercare un Person(da un database reale o da uno stub / mock). Quando si desidera verificare che la ricerca si integri con il resto del sistema, si è anche costretti a eseguire un calcolo sull'età. Cosa dovremmo fare con quel calcolo? Dovremmo ignorarlo o controllarlo? Questa sembra essere la situazione esatta che stai descrivendo nella tua domanda.

Se immaginiamo un'alternativa, potremmo avere il calcolo da soli:

def is_old_enough?(person)
   if person.male?
      return person.age > 21
   else 
      return person.age > 18
   end
end

Poiché si tratta di un calcolo puro, non è necessario eseguire test di integrazione su di esso.

Potremmo anche essere tentati di scrivere anche l'attività di ricerca separatamente:

def person_from_name(name = 'filip')
   return Person::API.new(name)
end

Tuttavia, in questo caso la funzionalità è così vicina Person::API.newche direi che dovresti usarla invece (se il nome predefinito è necessario, sarebbe meglio archiviato altrove, come un attributo di classe?).

Quando si scrivono test di integrazione per Person::API.new(o person_from_name) tutto ciò che occorre preoccuparsi è se si ritorna indietro al previsto Person; tutti i calcoli basati sull'età sono curati altrove, quindi i test di integrazione possono ignorarli.


11

Un altro punto che mi piace aggiungere alla risposta di Killian è che i test unitari vengono eseguiti molto rapidamente, quindi possiamo averne 1000. Un test di integrazione in genere richiede più tempo perché chiama servizi Web, database o altre dipendenze esterne, quindi non possiamo eseguire gli stessi test (1000) per scenari di integrazione in quanto richiederebbero troppo tempo.

Inoltre, in genere i test unitari vengono eseguiti al momento della compilazione (sulla macchina build) e i test di integrazione vengono eseguiti dopo la distribuzione su un ambiente / macchina.

In genere si eseguono i nostri migliaia di test unitari per ogni build, quindi i nostri test di integrazione di valore circa 100 dopo ogni implementazione. Potremmo non portare ciascuna build alla distribuzione, ma va bene perché la build che prendiamo per la distribuzione verrà eseguita i test di integrazione. In genere, vogliamo limitare l'esecuzione di questi test entro 10 o 15 minuti perché non vogliamo mantenere la distribuzione troppo a lungo.

Inoltre, su base settimanale possiamo eseguire una suite di regressione di test di integrazione che coprono più scenari nel fine settimana o in altri periodi di inattività. Questi possono richiedere più di 15 minuti poiché verranno trattati più scenari, ma in genere nessuno sta lavorando su Sat / Sun in modo da poter dedicare più tempo ai test.


non si applica ai linguaggi dinamici (ovvero senza fase di costruzione)
Filip Bartuzi,
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.