Cosa si intende sotto "unità" nei test unitari


9

Come ho capito in teoria sotto "unità" le persone significano metodo (in OOP). Ma in pratica i test che verificano alcuni metodi isolati sono test di comportamento molto fragili (verificando non il risultato ma il fatto che sia stato chiamato un metodo di dipendenza). Quindi vedo molte persone che per unità comprendono un piccolo insieme di classi strettamente correlate. In questo caso vengono derise / stub solo le dipendenze esterne e per le dipendenze che sono all'interno dell'unità vengono utilizzate implementazioni reali. In questo caso ci sono più test di stato, significativi (secondo le specifiche) e non così fragili. Quindi la domanda è: come ti senti riguardo a questi approcci ed è valido chiamare il test di unità secondo approccio o potrebbe essere una sorta di test di integrazione di basso livello?

Se vedi alcune considerazioni specifiche sull'applicazione del TDD in uno di questi modi di test, sarei grato per i tuoi pensieri.

Risposte:


11

Originariamente TDD proveniva dal movimento agile, in cui i test venivano scritti in anticipo per garantire che ciò che era stato codificato rimanesse corretto, date le specifiche che ora erano ben definite nel codice di test. Sembrava anche un aspetto molto importante del refactoring, in quanto quando si modificava il codice, si poteva fare affidamento sui test per dimostrare che non si era modificato il comportamento del codice.

Quindi gli strumenti che le persone hanno trovato e pensato di conoscere informazioni sul tuo codice e potrebbero quindi generare stub di test per aiutarti a scrivere i tuoi test unitari, e penso che sia qui che tutto è andato storto.

Gli stub di test sono generati da un computer che non ha idea di cosa stai facendo, produce semplicemente uno stub per ogni metodo perché è quello che gli viene detto di fare. Ciò significa che hai un caso di prova per ciascun metodo indipendentemente dalla complessità di quel metodo o se è adatto per il test in isolamento.

Questo sta arrivando ai test dall'estremità sbagliata della metodologia TDD. In TDD dovresti capire cosa deve fare il codice, e quindi produrre codice che raggiunga questo obiettivo. Questo si autoavvera nel fatto che finisci per scrivere test che dimostrano che il codice fa quello che fa il codice, non quello che dovrebbe fare. In combinazione con la generazione automatica di stub di test basati su metodi, praticamente perdi tempo a provare ogni piccolo aspetto del tuo codice che può facilmente rivelarsi sbagliato quando tutti i piccoli pezzi vengono messi insieme.

Quando Fowler descrisse i test nel suo libro, si riferì al test di ogni classe con il suo metodo principale. Ha migliorato questo, ma il concetto è sempre lo stesso: testate l'intera classe in modo che funzioni nel suo insieme, tutti i test sono raggruppati insieme per dimostrare l'interazione di tutti quei metodi in modo che la classe possa essere riutilizzata con aspettative definite.

Penso che i toolkit di test ci abbiano reso un disservizio, ci hanno portato lungo il percorso di pensare che il toolkit sia l'unico modo per fare le cose quando in realtà, devi pensare di più per te stesso per ottenere il miglior risultato dal tuo codice. Mettere ciecamente il codice di prova negli stub di test per piccoli pezzi significa solo che devi ripetere il tuo lavoro in un test di integrazione (e se hai intenzione di farlo, perché non saltare completamente la fase di test dell'unità ora ridondante). Significa anche che le persone sprecano molto tempo nel tentativo di ottenere una copertura del test al 100% e molto tempo nella creazione di grandi quantità di codice e dati beffardi che sarebbero stati meglio spesi per rendere il codice più semplice per il test di integrazione (cioè se hai così tanto dipendenze dei dati, unit test potrebbe non essere l'opzione migliore)

Infine, la fragilità dei test unitari basati su metodi mostra solo il problema. Il refactoring è progettato per essere utilizzato con i test unitari, se i test si interrompono continuamente perché stai effettuando il refactoring, allora qualcosa è andato storto in modo grave con l'intero approccio. Refactoring ama creare ed eliminare metodi, quindi ovviamente l'approccio test cieco per metodo non è quello che era originariamente previsto.

Non ho dubbi sul fatto che molti metodi avranno test scritti per loro, tutti i metodi pubblici di una classe dovrebbero essere testati, ma non puoi allontanarti dal concetto di testarli insieme come parte di un singolo caso di test. Ad esempio, se ho un set e un metodo get, posso scrivere dei test che inseriscono i dati e verificare che i membri interni siano impostati correttamente, oppure posso usare ciascuno di essi per inserire alcuni dati e quindi estrarli di nuovo per vedere se è sempre lo stesso e non confuso. Questo sta testando la classe, non ogni metodo in isolamento. Se il setter si basa su un metodo privato helper, allora va bene - non è necessario deridere il metodo privato per assicurarsi che il setter funzioni, non se si verifica l'intera classe.

Penso che la religione stia affrontando questo argomento, quindi si vede lo scisma in quello che ora è noto come sviluppo "guidato dal comportamento" e "guidato dai test" - il concetto originale di unit test era per lo sviluppo guidato dal comportamento.


10

Un'unità è comunemente definita come " la più piccola parte verificabile di un'applicazione ". Più spesso, sì, questo significa un metodo. E sì, questo significa che non dovresti testare il risultato di un metodo dipendente, ma solo che viene chiamato il metodo (e quindi solo una volta, se possibile, non in tutti i test per quel metodo).

Lo chiami fragile. Penso che sia errato. I test fragili sono quelli che si rompono con la minima modifica a codice non correlato. Cioè, quelli che fanno affidamento su un codice irrilevante per la funzionalità testata.

Tuttavia, ciò che penso che intendi davvero dire è che testare un metodo con nessuna delle sue dipendenze non è approfondito. Su questo punto sarei d'accordo. È inoltre necessario eseguire test di integrazione per assicurarsi che le unità di codice siano collegate correttamente per creare un'applicazione.

Questo è esattamente il problema che lo sviluppo guidato dal comportamento , e in particolare lo sviluppo guidato dai test di accettazione , si propone di risolvere. Ma ciò non toglie la necessità di unit test / sviluppo test-driven; lo completa semplicemente.


Volevo davvero dire che i test comportamentali sono fragili. Spesso diventano falsi negativi durante il cambio di base di codice. Succede meno con i test di stato (ma i test di stato sono molto rari per i test unitari)
SiberianGuy

@Idsa: sono un po 'perso dalle tue definizioni. I test comportamentali sono test di integrazione, testando un comportamento come specificato. Leggendo la tua domanda originale, sembra che quando dici test di stato, intendi la stessa cosa.
pdr

per stato intendo test che verifica lo stato, risultato di alcune funzioni; per comportamento intendo un test che verifica non il risultato, ma il fatto che sia stata chiamata una funzione
SiberianGuy

@Idsa: In tal caso, non sono completamente d'accordo. Quello che stai chiamando test di stato, chiamo integrazione. Quello che chiami comportamento, chiamo unità. I test di integrazione per loro stessa definizione sono più fragili. Google "unità di test di integrazione fragile" e vedrai che non sono solo.
pdr,

è disponibile un registro di articoli sui test ma quali di essi condividono la tua opinione?
SiberianGuy

2

Come suggerisce il nome, stai testando un soggetto atomico in ogni test. Tale argomento è di solito un singolo metodo. Test multipli possono testare lo stesso metodo, al fine di coprire il percorso felice, possibili errori, ecc. Stai testando il comportamento, non la meccanica interna. Quindi i test unitari riguardano davvero il test dell'interfaccia pubblica di una classe, ovvero un metodo specifico.

Nel test unitario, un metodo deve essere testato in isolamento, cioè stub / derisione / falsificazione di eventuali dipendenze. Altrimenti, testare un'unità con dipendenze "reali" la rende un test di integrazione. C'è un tempo e un luogo per entrambi i tipi di test. I test unitari assicurano che un singolo soggetto funzioni come previsto, autonomo. I test di integrazione assicurano che soggetti "reali" lavorino insieme correttamente.


1
non del tutto, un'unità è un soggetto isolato, solo perché gli strumenti automatizzati preferiscono trattare un metodo in quanto ciò non lo rende, né lo rende il migliore. 'Isolata' è la chiave qui. Anche se testate metodi, dovreste testare anche quelli privati.
gbjbaanb

1

La mia regola empirica: la più piccola unità di codice che è ancora abbastanza complessa da contenere bug.

Che si tratti di un metodo, di una classe o di un sottosistema, dipende dal codice specifico, non è possibile specificare una regola generale.

Ad esempio, non fornisce alcun valore per testare i semplici metodi getter / setter in isolamento o metodi wrapper che chiamano solo un altro metodo. Anche un'intera classe potrebbe essere troppo semplice per essere testata, se la classe è solo un wrapper o un adattatore sottile. Se l'unica cosa da testare è se viene chiamato un metodo su un finto, allora il codice sotto test è troppo sottile.

In altri casi, un singolo metodo potrebbe eseguire alcuni calcoli complessi che è utile testare isolatamente.

In molti casi le parti complesse non sono singole classi ma piuttosto l'integrazione tra le classi. Quindi testerai due o più classi contemporaneamente. Alcuni diranno che non si tratta di unit test ma di test di integrazione, ma non importa la terminologia: dovresti testare dove si trova la complessità e questi test dovrebbero far parte della suite di test.

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.