Tipi di test unitari basati sull'utilità


13

Dal punto di vista del valore vedo due gruppi di unit test nella mia pratica:

  1. Test che testano una logica non banale. Scriverli (prima o dopo l'implementazione) rivela alcuni problemi / potenziali bug e aiuta ad essere sicuri nel caso in cui la logica venga modificata in futuro.
  2. Test che mettono alla prova una logica molto banale. Questi test assomigliano più al codice del documento (tipicamente con simulazioni) che al test. Il flusso di lavoro di manutenzione di quei test non è "un po 'di logica è cambiata, il test è diventato rosso - grazie a Dio ho scritto questo test" ma "un po' di codice banale è cambiato, il test è diventato falso negativo - Devo mantenere (riscrivere) il test senza ottenere alcun profitto" . Il più delle volte questi test non valgono la pena di essere mantenuti (tranne per motivi religiosi). E secondo la mia esperienza in molti sistemi, questi test sono come l'80% di tutti i test.

Sto cercando di scoprire cosa pensano gli altri ragazzi sull'argomento della separazione dei test unitari in base al valore e in che modo corrisponde alla mia separazione. Ma quello che vedo principalmente è la propaganda TDD a tempo pieno o la propaganda test-are-inutile-basta-scrivere-il-codice. Sono interessato a qualcosa nel mezzo. I tuoi pensieri o riferimenti ad articoli / documenti / libri sono i benvenuti.


3
Continuo a test unitari alla ricerca di bug noti (specifici) - che una volta sfogliavano il set di test unitari originali - come gruppo separato il cui ruolo è prevenire bug di regressione.
Konrad Morawski,

6
Quel secondo tipo di test è quello che considero una sorta di "cambio di attrito". Non scartare la loro utilità. Cambiare anche le banalità del codice tende ad avere effetti a catena in tutta la base di codice e l'introduzione di questo tipo di attrito agisce come un ostacolo per i tuoi sviluppatori in modo che cambino solo le cose che ne hanno davvero bisogno, piuttosto che sulla base di alcune preferenze stravaganti o personali.
Telastyn,

3
@Telastyn - Tutto ciò che riguarda il tuo commento mi sembra assolutamente pazzo. Chi renderebbe deliberatamente difficile cambiare il codice? Perché scoraggiare gli sviluppatori dal cambiare il codice quando lo ritengono opportuno - non ti fidi di loro? Sono cattivi sviluppatori?
Benjamin Hodgson,

2
In ogni caso, se la modifica del codice tende ad avere "effetti a catena", allora il tuo codice ha un problema di progettazione - nel qual caso gli sviluppatori dovrebbero essere incoraggiati a refactoring quanto è ragionevole. I test fragili scoraggiano attivamente il refactoring (un test fallisce; chi può preoccuparsi di capire se quel test era uno dell'80% dei test che non fanno davvero nulla? Trovi semplicemente un modo diverso e più complicato per farlo). Ma sembri vederlo come una caratteristica desiderabile ... Non capisco affatto.
Benjamin Hodgson,

2
Ad ogni modo, l'OP potrebbe trovare interessante questo post sul blog del creatore di Rails. Per semplificare eccessivamente il suo punto, dovresti probabilmente provare a buttare via l'80% dei test.
Benjamin Hodgson,

Risposte:


14

Penso che sia naturale incontrare una divisione all'interno dei test unitari. Ci sono molte opinioni diverse su come farlo correttamente e naturalmente tutte le altre opinioni sono intrinsecamente sbagliate . Di recente ci sono alcuni articoli su DrDobbs che esplorano proprio questo problema al quale mi collego alla fine della mia risposta.

Il primo problema che vedo con i test è che è facile sbagliarli. Nella mia classe C ++ al college siamo stati esposti a test unitari sia nel primo che nel secondo semestre. Non sapevamo nulla della programmazione in generale in nessuno dei due semestri: stavamo cercando di apprendere i fondamenti della programmazione tramite C ++. Ora immagina di dire agli studenti: "Oh, ehi, hai scritto un piccolo calcolatore fiscale annuale! Ora scrivi alcuni test unitari per assicurarti che funzioni correttamente." I risultati dovrebbero essere ovvi: erano tutti orribili, compresi i miei tentativi.

Una volta che ammetti di fare schifo durante la scrittura di unit test e desideri migliorare, ti troverai presto di fronte a stili di test alla moda o metodologie diverse. Testando metodologie mi riferisco a pratiche come test-first o cosa fa Andrew Binstock di DrDobbs, che è scrivere i test insieme al codice. Entrambi hanno i loro pro e contro e mi rifiuto di entrare in qualsiasi dettaglio soggettivo perché ciò inciterà una guerra di fiamma. Se non sei confuso su quale sia la metodologia di programmazione migliore, forse lo stile dei test farà il trucco. Dovresti usare TDD, BDD, Test basati su proprietà? JUnit ha concetti avanzati chiamati Teorie che confondono la linea tra TDD e test basati su proprietà. Quale usare quando?

tl; dr È facile sbagliare nei test, è incredibilmente supponente e non credo che nessuno dei metodi di test sia intrinsecamente migliore purché siano usati diligentemente e professionalmente nel contesto in cui sono appropriati. Inoltre, i test sono nella mia mente un'estensione alle asserzioni o ai test di sanità mentale usati per garantire un approccio allo sviluppo ad hoc ad alta velocità che ora è molto, molto più semplice.

Per un'opinione soggettiva, preferisco scrivere "fasi" di test, per mancanza di una frase migliore. Scrivo unit test che testano le classi in isolamento, usando derisioni ove necessario. Questi verranno probabilmente eseguiti con JUnit o qualcosa di simile. Quindi scrivo test di integrazione o accettazione, questi vengono eseguiti separatamente e di solito solo poche volte al giorno. Questi sono i tuoi casi d'uso non banali. Di solito uso BDD perché è bello esprimere funzionalità in linguaggio naturale, cosa che JUnit non può facilmente fornire.

Infine, risorse. Questi presenteranno opinioni contrastanti principalmente incentrate sui test unitari in diverse lingue e con diversi framework. Dovrebbero presentare la divisione in ideologia e metodologia mentre ti permetteranno di formulare la tua opinione fintanto che non ho già manipolato troppo la tua :)

[1] The Corruption of Agile di Andrew Binstock

[2] Risposta alle risposte dell'articolo precedente

[3] Risposta alla corruzione di Agile da parte di zio Bob

[4] Risposta alla corruzione di Agile di Rob Myers

[5] Perché preoccuparsi dei test sui cetrioli?

[6] Stai sbagliando

[7] Allontanati dagli strumenti

[8] Commento su "Numeri romani Kata con commento"

[9] Numeri romani Kata con commento


1
Una delle mie amichevoli affermazioni sarebbe che se stai scrivendo un test per testare la funzione di un calcolatore fiscale annuale, allora non stai scrivendo un test unitario. Questo è un test di integrazione. Il tuo calcolatore dovrebbe essere suddiviso in unità di esecuzione abbastanza semplici e i test delle unità quindi testano tali unità. Se una di quelle unità smette di funzionare correttamente (il test inizia a fallire), allora è come buttare giù una parte di un muro di fondazione, ed è necessario riparare il codice (non il test, in generale). O quello, o hai identificato un po 'di codice che non è più necessario e dovrebbe essere scartato.
Craig,

1
@Craig: Precisamente! Questo è ciò che intendevo con non sapere come scrivere test adeguati. Come studente universitario, l'esattore delle tasse era una classe di grandi dimensioni scritta senza una corretta comprensione di SOLID. Hai assolutamente ragione pensando che si tratti più di un test di integrazione che di ogni altra cosa, ma per noi era un termine sconosciuto. Siamo stati esposti ai test "unit" solo dal nostro professore.
IAE

5

Ritengo sia importante effettuare test di entrambi i tipi e utilizzarli laddove appropriato.

Come hai detto, ci sono due estremi e onestamente non sono d'accordo con nessuno dei due.

La chiave è che i test unitari devono coprire le regole e i requisiti aziendali . Se è necessario che il sistema tenga traccia dell'età di una persona, scrivere test "banali" per assicurarsi che l'età sia un numero intero non negativo. Stai testando il dominio dei dati richiesti dal sistema: sebbene banale, ha valore perché impone i parametri del sistema .

Allo stesso modo con test più complessi, devono apportare valore. Certo, puoi scrivere un test che convalida qualcosa che non è un requisito ma che dovrebbe essere applicato da qualche parte in una torre d'avorio, ma che è meglio dedicare tempo a scrivere test che convalidi i requisiti per i quali il cliente ti sta pagando. Ad esempio, perché scrivere un test che convalida il codice può gestire un flusso di input che scade, quando gli unici flussi provengono da file locali, non dalla rete?

Credo fermamente nei test unitari e utilizzo TDD ovunque abbia senso. I test unitari apportano sicuramente valore sotto forma di maggiore qualità e comportamento "fail fast" quando si modifica il codice. Tuttavia, c'è anche la vecchia regola 80/20 da tenere a mente. Ad un certo punto otterrai rendimenti decrescenti durante la scrittura dei test e dovrai passare a un lavoro più produttivo anche se c'è un valore misurabile da ottenere dalla scrittura di più test.


Scrivere un test per assicurarsi che un sistema tenga traccia dell'età di una persona non è un test unitario, IMO. Questo è un test di integrazione. Un test unitario verificherebbe l'unità generica di esecuzione (detta anche "procedura") che, per esempio, calcola un valore di età da, ad esempio, una data di base e una compensazione in qualunque unità (giorni, settimane, ecc.). Il mio punto è che un po 'di codice non dovrebbe avere strane dipendenze in uscita dal resto del sistema. Calcola SOLO un'età a partire da un paio di valori di input, e in tal caso un test unitario può confermare il comportamento corretto, il che probabilmente genera un'eccezione se l'offset produce un'età negativa.
Craig,

Non mi riferivo a nessun calcolo. Se un modello memorizza una parte di dati, può convalidare i dati appartenenti al dominio corretto. In questo caso, il dominio è l'insieme di numeri interi non negativi. I calcoli dovrebbero avvenire nel controller (in MVC) e in questo esempio un calcolo dell'età sarebbe un test separato.

4

Ecco la mia opinione: tutti i test hanno dei costi:

  • tempo e sforzo iniziali:
    • pensa a cosa testare e come testarlo
    • implementare il test e assicurarsi che stia testando ciò che dovrebbe
  • manutenzione in corso
    • assicurarsi che il test stia ancora facendo quello che dovrebbe fare mentre il codice si evolve naturalmente
  • eseguendo il test
    • tempo di esecuzione
    • analizzando i risultati

Intendiamo inoltre che tutti i test offrano benefici (e nella mia esperienza, quasi tutti i test forniscono benefici):

  • specificazione
  • evidenziare casi d'angolo
  • prevenire la regressione
  • verifica automatica
  • esempi di utilizzo dell'API
  • quantificazione di proprietà specifiche (tempo, spazio)

Quindi è abbastanza facile vedere che se scrivi un sacco di test, probabilmente avranno un certo valore. Il punto in cui questo diventa complicato è quando inizi a confrontare quel valore (che, a proposito, potresti non sapere in anticipo - se butti via il tuo codice, i test di regressione perdono il loro valore) con il costo.

Ora, il tuo tempo e il tuo sforzo sono limitati. Ti piacerebbe scegliere di fare quelle cose che offrono il massimo beneficio al minor costo. E penso che sia una cosa molto difficile da fare, anche perché potrebbe richiedere una conoscenza che non si ha o sarebbe costosa da ottenere.

E questo è il vero problema tra questi diversi approcci. Credo che abbiano identificato tutte le strategie di test utili. Tuttavia, ogni strategia ha costi e benefici diversi in generale. Inoltre, i costi e i benefici di ciascuna strategia dipenderanno probabilmente in larga misura dalle specifiche del progetto, dal dominio e dal team. In altre parole, potrebbero esserci più risposte migliori.

In alcuni casi, l'estrazione del codice senza test può fornire i migliori benefici / costi. In altri casi, una suite di test approfondita potrebbe essere migliore. In altri casi, migliorare il design può essere la cosa migliore da fare.


2

Che cosa è un'unità di prova, davvero? E c'è davvero una così grande dicotomia in gioco qui?

Lavoriamo in un campo in cui la lettura letteralmente un po 'oltre la fine di un buffer può causare l'arresto anomalo totale di un programma o causare un risultato totalmente inaccurato o, come evidenziato dal recente bug TLS "HeartBleed", a livello di un sistema apparentemente sicuro aperto senza produrre alcuna prova diretta del difetto.

È impossibile eliminare tutta la complessità da questi sistemi. Ma il nostro lavoro è, per quanto possibile, minimizzare e gestire quella complessità.

Un test unitario è un test che conferma, ad esempio, che una prenotazione è stata registrata correttamente in tre sistemi diversi, viene creata una voce di registro e viene inviata una conferma e-mail?

Sto per dire di no . Questo è un test di integrazione . E quelli sicuramente hanno il loro posto, ma sono anche un argomento diverso.

Un test di integrazione funziona per confermare la funzione complessiva di un'intera "caratteristica". Ma il codice alla base di tale funzione dovrebbe essere suddiviso in blocchi semplici e testabili, noti anche come "unità".

Quindi un test unitario dovrebbe avere un ambito molto limitato.

Ciò implica che il codice testato dal test unitario dovrebbe avere un ambito molto limitato.

Ciò implica inoltre che uno dei pilastri della buona progettazione è quello di scomporre il tuo problema complesso in pezzi più piccoli e monouso (per quanto possibile) che possono essere testati in un relativo isolamento l'uno dall'altro.

Ciò che si ottiene è un sistema costituito da componenti di fondazione affidabili e si sa se una di quelle unità fondamentali di codice si rompe perché hai scritto test semplici, piccoli e di portata limitata per dirti esattamente questo.

In molti casi probabilmente dovresti anche avere più test per unità. I test stessi dovrebbero essere semplici, testando uno e un solo comportamento per quanto possibile.

L'idea di un "test unitario" per testare una logica non banale, elaborata e complessa è, a mio avviso, un po 'un ossimoro.

Quindi, se quel tipo di rottura intenzionale del progetto ha avuto luogo, come potrebbe un test unitario iniziare improvvisamente a produrre falsi positivi, a meno che la funzione di base dell'unità di codice testata non sia cambiata? E se ciò è accaduto, allora è meglio credere che ci siano alcuni effetti a catena non ovvi in ​​gioco. Il test interrotto, quello che sembra produrre un falso positivo, in realtà ti avverte che alcuni cambiamenti hanno spezzato un cerchio più ampio di dipendenze nella base di codice e devono essere esaminati e risolti.

Alcune di queste unità (molte) potrebbero dover essere testate usando oggetti finti, ma ciò non significa che devi scrivere test più complessi o elaborati.

Tornando al mio esempio inventato di un sistema di prenotazione, non puoi davvero inviare richieste a un database di prenotazione dal vivo o a un servizio di terze parti (o persino a un'istanza "dev" di esso) ogni volta che esegui il test del tuo codice.

Quindi usi mock che presentano lo stesso contratto di interfaccia. I test possono quindi validare il comportamento di un pezzo di codice relativamente piccolo e deterministico. Il verde lungo tutto il tabellone ti dice quindi che i blocchi che compongono la tua fondazione non sono rotti.

Ma la logica dei singoli test unitari rimane il più semplice possibile.


1

Questo è ovviamente solo il mio parere, ma aver trascorso gli ultimi mesi a studiare la programmazione funzionale in fsharp (proveniente da un background C #) mi ha fatto capire alcune cose.

Come affermato dall'OP, in genere ci sono 2 tipi di "test unitari" che vediamo ogni giorno. Test che coprono l'entrata e l'uscita di un metodo, che sono generalmente i più preziosi, ma sono difficili da fare per l'80% del sistema, che riguarda meno gli "algoritmi" e più le "astrazioni".

L'altro tipo, sta testando l'interattività di astrazione, in genere comporta beffardo. A mio avviso, questi test sono principalmente necessari a causa della progettazione della tua applicazione. Ommitendoli e rischi di strani bachi e codice spagetti, perché le persone non pensano al loro design in modo corretto a meno che non siano costrette a fare i test prima (e anche allora, di solito sbagliano). Il problema non è tanto la metodologia di test, ma la progettazione di base del sistema. La maggior parte dei sistemi costruiti con linguaggi imperativi o OO ha una dipendenza intrinseca da "effetti collaterali", ovvero "Fallo, ma non dirmi niente". Quando si fa affidamento sull'effetto collaterale, è necessario testarlo, poiché un requisito o un'operazione aziendale di solito ne fa parte.

Quando si progetta il sistema in un modo più funzionale, in cui si evita di creare dipendenze dagli effetti collaterali e si evitano cambiamenti / tracciamento dello stato attraverso l'immutabilità, si consente di concentrarsi maggiormente sui test "in e out", che testano chiaramente più l'azione e meno come ci si arriva. Sarai sorpreso di ciò che cose come l'immutabilità possono darti in termini di soluzioni molto più semplici agli stessi problemi e quando non sei più dipendente da "effetti collaterali" puoi fare cose come la programmazione parallela e asincrona senza quasi nessun costo aggiuntivo.

Da quando ho iniziato a scrivere codice in Fsharp, non ho avuto bisogno di un framework beffardo per nulla e ho persino perso completamente la mia dipendenza da un contenitore IOC. I miei test sono guidati dalle esigenze e dal valore dell'azienda e non su livelli di astrazione pesanti in genere necessari per ottenere una composizione nella programmazione imperativa.

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.