I test unitari dovrebbero essere interamente autonomi? [duplicare]

1

Come suggerisce il titolo, la mia domanda è se o non i test unitari dovrebbero essere interamente autosufficienti o possibili uno si basa sui risultati ottenuti dai test precedenti?

Cosa intendo, nel caso in cui non sia completamente chiaro, è che se il test iniziale afferma sufficientemente che un certo modulo A funziona in un certo modo può, o più appropriatamente, se si scrive successivamente prova con l'ipotesi che il suddetto il modulo A è stato testato in anticipo? Questo implicherebbe questo l'ordine in cui vengono eseguiti i test unitari è importante.

O ogni singolo test dovrebbe essere autosufficiente al punto che se è necessario sapere se a il modulo B funziona, che può essere validato se il modulo A funziona, si dovrebbe testare il modulo A all'interno dello stesso test del modulo B che implicherebbe che i test unitari separati possano essere eseguito in qualsiasi ordine.

Per dare un esempio concreto, si consideri il tipo di dati "stack", che non approfondiremo troppo in profondità, in particolare due proprietà fondamentali che dovrebbero essere in grado di ragionare sul tipo di dati in qualsiasi senso significativo. Vale a dire, isEmpty(stack) e Empty() . Ora, se si desidera testare la validità di isEmpty che prende uno stack e restituisce un True o False a seconda che lo stack ricevuto o meno come argomento sia vuoto o meno si dovrebbe prima creare un vuoto impila utilizzando Empty() .

Quindi consideriamo lo scenario di fare quanto segue: isEmpty(Empty()) e verificare quale tipo di risultato otteniamo. O otteniamo un'istruzione True , quindi Empty() potrebbe avere restituito uno stack non vuoto e isEmpty potrebbe visualizzarlo come uno stack vuoto che sarebbe sbagliato. Oppure otteniamo False , e ancora non sappiamo come i due interagiscano realmente, né come funzionino da soli (supponendo che non possiamo vedere la fonte). (C'è una terza opzione, riceviamo qualcosa che non è né TrueFalse , ma che è oltre lo scopo di questa discussione, questa è solo un'osservazione).

Infine, per legare questo alla mia domanda, se creiamo un test in cui possiamo essere ragionevolmente certi che isEmpty funzioni in modo soddisfacente, allora tutti i test eseguiti dopo aver fiducia che effettivamente funziona? O dovrebbero provare tutti e incorporare questa ambiguità nella propria logica di test (ad esempio, includendo un'altra istruzione se non è stato restituito né TrueFalse )

    
posta Bart van Ingen Schenau 01.02.2014 - 14:59
fonte

5 risposte

8

Ogni test di unità dovrebbe testare una cosa, quindi sì, dovrebbero presupporre che tutte le altre parti del sistema stiano funzionando. Il modo per farlo in modo affidabile è mock qualsiasi altro codice che non dovrebbe essere testato allo stesso tempo.

Ad esempio, considera questo pseudo-codice:

house_json(id):
    name = get_house_name(id)
    return json.format({'id': id, 'name': name})

Dipende dalla funzionalità di altre due funzioni: get_house_name e json.format . Per testare l'unità dovrai prendere in giro entrambi di loro. Prima il caso normale, testando che con un ID di casa valido finiamo per chiamare json.format con i parametri previsti:

test_house_json_format():
    get_house_name = mock()
    json.format = mock()
    get_house_name.return_for(5) = 'foo'
    house_json(5)
    assert_called_once_with(json.format, {'id': 5, 'name': 'foo'})

Quindi per verificare che se inviamo un ID di casa non valido, il programma di formattazione dovrebbe generare un'eccezione:

test_house_json_with_invalid_house_id():
    get_house_name = mock()
    json.format = mock()
    get_house_name.return(x) = lambda: raise InvalidHouseError(x)
    assert_raises(house_json, 5, InvalidHouseError)
    assert_not_called(json.format)

Rendere esplicite le aspettative sul codice nel tuo codice di test significa che sarà facile per gli altri capire il suo comportamento previsto e cambiarlo quando cambiano i requisiti. Ad esempio, se la funzione deve gestire gli errori generati e restituire un codice di errore in formato JSON, lo cambierai in questo test:

test_house_json_with_invalid_house_id():
    get_house_name, json.format = mock()
    get_house_name.return(x) = lambda: raise InvalidHouseError(x)
    house_json(5)
    assert_called_once_with(json.format, {'invalid_house_error': 5})

Quindi eseguirai il test per verificare che fallisca, corregga get_house_name per farlo passare di nuovo e refactoring per renderlo il codice più semplice possibile che passa (red-green-refactoring).

Dopo aver provato tutte le parti, dovresti aggiungere test di integrazione e accettazione senza fare il mocking per assicurarti che funzionino tutti insieme.

    
risposta data 01.02.2014 - 16:08
fonte
6

In primo luogo, è una buona pratica assicurarsi che i test automatici siano indipendenti dall'ordine di esecuzione (ad esempio, quando un test fallisce, si desidera eseguire esattamente quel test nel debugger senza avere altri 10 test da eseguire prima).

Ma hai chiesto qualcosa di diverso:

is that if ones initial test sufficiently asserts that a certain module A works in a certain way [], should one write subsequent tests with the assumption that the aforementioned module A is tested beforehand

Scrivendo un test per una funzione X che si basa sulla correttezza della funzione Y non rende i tuoi test dipendenti dall'ordine. Se la tua funzione X è sbagliata, i tuoi test per Y potrebbero fallire (così come i tuoi test per la funzione Y), e se falliscono, falliscono sempre se esegui i test per la funzione X, o se non li esegui, non importa.

    
risposta data 01.02.2014 - 17:31
fonte
3

Direi che dovresti enfaticamente non rendere i tuoi test individuali completamente autosufficienti. In genere non è utile provare a scrivere test sul modulo B che funzionerà anche se il modulo A è buggato. In effetti, penso che sia spesso pericoloso e poco saggio tentare di farlo.

Un test unitario rigoroso deriderà tutte le altre classi oltre a quella in fase di test. Pertanto il test specificherà gli ingressi e le uscite a tutti gli oggetti / moduli diversi da quello in prova. Non penso che ci sia un grande valore nello scrivere questi severi test unitari. Ci sono casi per prendere in giro i moduli, ma in generale si dovrebbe usare di default il modulo reale.

Perché?

1) Eseguendo l'effettiva implementazione del modulo A mentre si esegue il test del modulo B, si ottengono test aggiuntivi sul modulo A gratuitamente. I bug che non hai catturato durante il test del modulo A possono essere utilizzati nel modulo B. Stai buttando via preziosi controlli sull'accuratezza del modulo A se semplicemente lo prendi in giro.

2) Schernire tutte le chiamate esterne è odioso. Si finisce per dover scrivere molto codice nel test per specificare gli input e gli output dei vari oggetti utilizzati. Questo è in genere noioso e rende i test più difficili da leggere e scrivere.

3) Se si prendono in giro le chiamate al modulo A dal modulo B, si asserisce che il modulo B ha effettuato le chiamate previste. Non stai controllando che queste siano state le chiamate corrette da fare. Ad esempio, supponi di avere una funzione come:

Foobar highestScoring() {
    // getFoobars() returns the Foobars sorted by score.
    return module_a.getFoobars().getFirst();
}

Sembra abbastanza ragionevole, ma quale ordine fa module_a per ordinarli? Se ordina da più basso a più alto, questa funzione è sbagliata. Ma se prendeste in giro module_a, avreste perso questo perché pensavate che fosse risolto il contrario. Questo è un bug nel Modulo A che sarebbe mancato nel tentativo di isolarlo dal Modulo B.

Quali sono i vantaggi di prendere in giro ogni modulo? Ci sono sicuramente dei vantaggi nel prendere in giro certi moduli. In genere si desidera simulare moduli che rallentano, cambiano rapidamente o causerebbero effetti collaterali. Ma cosa fa la pratica di un rigoroso test unitario che prende in giro tutto ciò che ti prende? La teoria è che se il modulo A è bacato, i test punteranno al modulo A e non a tutto ciò che dipende dal modulo A.

Non penso che questo sia molto utile nella pratica. Se hai effettivamente rotto il modulo A, in genere è perché hai modificato il modulo A. Quindi probabilmente già conosci il modulo A danneggiato, perché è per questo che sono state apportate modifiche.

In realtà, se davvero volessimo quel beneficio, le piattaforme di testing unitario potrebbero aggiungerlo annunciando o inferendo una dipendenza tra i test. Quindi, se il test del modulo A fallisce, non eseguiremo neanche entrambi i test del modulo B.

Quindi in breve, sistemare in modo sistematico ogni modulo del test richiede molto lavoro, crea punti da nascondere per i bug e offre vantaggi marginali. Basta non farlo.

    
risposta data 01.02.2014 - 19:16
fonte
1

L'obiettivo del test unitario è testare tutti i percorsi all'interno di una classe. Ciò significa testare tutti i possibili input (o campioni rappresentativi), ma anche tutti i possibili comportamenti delle dipendenze, inclusi i fallimenti di dipendenza, cioè come un metodo nel modulo B gestisce tutti i possibili valori corretti restituiti da A, tutte le possibili eccezioni lanciate da A e tutte le possibili valori di ritorno errati da A, ad es nullo. Il test del modulo B facendo affidamento su una serie di test per A è valido fino a quando si riconosce che rappresenta solo un sottoinsieme di comportamenti di A (a meno che non si possa confermare che è possibile esercitare tutti i percorsi in A utilizzando l'intervallo di input per B). Per testare completamente B, è probabile che sia necessario forzare l'intera gamma di valori / eccezioni di ritorno potenziali da A facendolo beffe. Non importa dalla prospettiva di B se A è corretto o no. Ciò che conta è il modo in cui B gestisce qualsiasi implementazione di A, ovvero per proteggere da implementazioni di bug attuali o future di A (o delle dipendenze di A). L'obiettivo è dimostrare che B fa sempre la cosa giusta per gli input corretti e per i comportamenti di dipendenza corretti, e fa anche qualcosa di sensato (non blocca o corrompe nulla, informa il chiamante del problema, restituisce il controllo con garbo) quando riceve input o incontri sbagliati comportamento scorretto da parte di una dipendenza.

Quindi la mia risposta è che i test unitari possono usare altri test unitari per generare il comportamento delle dipendenze, ma che è probabilmente necessario un mockup di dipendenze per definire un set completo di test unitari, ad es. includere i fallimenti di dipendenza. La particolare implementazione delle dipendenze e la loro correttezza sono irrilevanti, solo le loro API sono importanti.

Per dirla in modo leggermente diverso, un'eccezione generata da A è un test unitario valido di B in quanto verifica un percorso di gestione degli errori in B, che è irraggiungibile variando l'input in B.

    
risposta data 02.02.2014 - 02:31
fonte
0

I test unitari dovrebbero essere autonomi, ma non devono essere indipendenti dall'ordine. Dovrebbe essere possibile un test in isolamento dagli altri, ma un fallimento può quindi implicare un fallimento in una dipendenza, non nel test stesso.

Fai un esempio semplicistico, una serie di funzioni che implementano un tipo di dati. Due di queste funzioni sono Parse () e Format (), che converte da e in una rappresentazione di stringa. È una strategia abbastanza ragionevole per testare la funzione Format () e quindi utilizzare la funzione Format () nei test sulla funzione Parse (). Se i test sono esauriti, un errore nella funzione Format () potrebbe effettivamente apparire come un errore di test nella funzione Parse ().

Questo è semplicistico, ma è comune creare suite di test sempre più complessi sulla base della conoscenza che i test precedenti hanno superato. In alcuni casi, il mocking è una strategia migliore, ma è necessario testare gli stessi componenti simulati, quindi anche in questo caso si dipenderà da test con esito positivo precedente.

    
risposta data 02.02.2014 - 02:06
fonte

Leggi altre domande sui tag