I test di integrazione sono pensati per ripetere tutti i test unitari?

34

Diciamo che ho una funzione (scritta in Ruby, ma dovrebbe essere comprensibile da 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

Nei test unitari creerei quattro test per coprire tutti gli scenari. Ciascuno utilizzerà l'oggetto% co_de mocked con i metodi stub Person::API e male? .

Ora si tratta di scrivere test di integrazione. Suppongo che Person :: API non debba essere più deriso. Quindi creerei esattamente gli stessi quattro casi di test, ma senza prendere in giro l'oggetto Person :: API. È corretto?

Se sì, allora qual è il punto di scrivere test unitari, se potessi scrivere solo test di integrazione che mi danno più sicurezza (dato che lavoro su oggetti reali, non su stub o mock)?

    
posta Filip Bartuzi 06.06.2016 - 12:32
fonte

3 risposte

71

No, i test di integrazione dovrebbero non solo duplicare la copertura dei test unitari. possono duplicare una certa copertura, ma non è questo il punto.

Il punto di un test unitario è quello di garantire che uno specifico piccolo bit di funzionalità funzioni esattamente e completamente come previsto. Un test unitario per am_i_old_enough testerebbe i dati con età diverse, certamente quelle vicine alla soglia, possibilmente tutte in età umane. Dopo aver scritto questo test, l'integrità di am_i_old_enough non dovrebbe mai essere nuovamente in discussione.

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

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

Il motivo per cui abbiamo bisogno di entrambi i tipi di test è fondamentalmente che esiste un'esplosione combinatoria di possibili scenari per il percorso attraverso un codice base che l'esecuzione potrebbe richiedere. Se la funzione di utilità ha circa 100 ingressi possibili e ci sono centinaia di funzioni di utilità, quindi controllare che la cosa giusta si verifichi in casi di tutti richiederebbe molti, molti milioni di casi di test. Semplicemente controllando tutti i casi in ambiti molto piccoli e quindi controllando le combinazioni comuni, rilevanti o probabili di questi ambiti, assumendo che questi piccoli ambiti siano già corretti, come dimostrato dai test unitari , possiamo ottenere una valutazione abbastanza fiduciosa che il sistema sta facendo ciò che dovrebbe, senza annegamento in scenari alternativi da testare.

    
risposta data 06.06.2016 - 12:48
fonte
12

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

Penso che la confusione stia sorgendo perché stai cercando di aderire a rigorose pratiche di test (test unitari vs test di integrazione, derisione, ecc.) per 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 test potrebbero non essere applicabili in questa situazione e potrebbe aiutare a utilizzare un livello simile di "rigore" nelle pratiche di codifica e nelle pratiche di test; o almeno, per riconoscere che potrebbero essere sbilanciati, il che renderà alcuni aspetti inapplicabili o ridondanti.

La ragione più ovvia è che la tua funzione sta svolgendo due diversi compiti:

  • Ricerca di un Person in base al loro nome. Ciò richiede test di integrazione, per assicurarci che possa trovare Person oggetti che sono presumibilmente creati / archiviati altrove.
  • Calcolo se Person è abbastanza vecchio, in base al sesso. Ciò richiede il test dell'unità, per assicurarsi che il calcolo si comporti come previsto.

Raggruppando queste attività insieme in un blocco di codice, non è possibile eseguirne uno senza l'altro. Quando vuoi testare i calcoli unitari, sei costretto a cercare un Person (da un vero database o da uno stub / mock). Quando vuoi verificare che la ricerca si integri con il resto del sistema, devi anche 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 solo:

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 separatamente l'attività di ricerca:

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

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

Durante la scrittura di test di integrazione per Person::API.new (o person_from_name ) tutto ciò di cui ti devi preoccupare è se torni indietro del Person previsto; tutti i calcoli basati sull'età sono presi in considerazione altrove, quindi i test di integrazione possono ignorarli.

    
risposta data 06.06.2016 - 18:23
fonte
10

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

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

In genere, eseguiremo i nostri 1000 test unitari per ogni build e quindi i nostri test di integrazione di 100 o più valori dopo ogni distribuzione. Non possiamo prendere ogni build per la distribuzione, ma va bene perché verrà eseguita la build che utilizzeremo per l'implementazione dei test di integrazione. In genere, vogliamo limitare questi test all'esecuzione entro 10 o 15 minuti perché non vogliamo ritardare la distribuzione troppo a lungo.

Inoltre, su base settimanale possiamo eseguire una suite di regressione di test di integrazione che coprono più scenari durante il fine settimana o altri tempi di inattività. Questi possono richiedere più di 15 minuti in quanto saranno coperti più scenari, ma in genere nessuno sta lavorando su Sat / Sun, quindi possiamo impiegare più tempo con i test.

    
risposta data 06.06.2016 - 17:39
fonte

Leggi altre domande sui tag