Cosa rende un buon test unitario? [chiuso]


97

Sono sicuro che la maggior parte di voi stia scrivendo molti test automatizzati e che durante i test unitari si sia anche imbattuto in alcune insidie ​​comuni.

La mia domanda è: segui delle regole di condotta per scrivere i test al fine di evitare problemi in futuro? Per essere più precisi: quali sono le proprietà di buoni unit test o come scrivi i tuoi test?

I suggerimenti indipendenti dalla lingua sono incoraggiati.

Risposte:


93

Vorrei iniziare collegando le fonti - Pragmatic Unit Testing in Java con JUnit (c'è anche una versione con C # -Nunit .. ma ho questa .. è agnostico per la maggior parte. Consigliato.)

I buoni test dovrebbero essere UN VIAGGIO (l'acronimo non è abbastanza appiccicoso - ho una stampa del cheatsheet nel libro che ho dovuto tirare fuori per assicurarmi di aver capito bene ..)

  • Automatico : il richiamo dei test e il controllo dei risultati per SUPERATO / NON RIUSCITO dovrebbero essere automatici
  • Completo : copertura; Sebbene i bug tendano a raggrupparsi attorno a determinate aree del codice, assicurati di testare tutti i percorsi e gli scenari chiave. Utilizza gli strumenti se devi conoscere le aree non testate
  • Ripetibile : i test dovrebbero produrre gli stessi risultati ogni volta .. ogni volta. I test non dovrebbero basarsi su parametri incontrollabili.
  • Indipendente : molto importante.
    • I test dovrebbero testare solo una cosa alla volta. Asserzioni multiple vanno bene fintanto che testano tutte una caratteristica / comportamento. Quando un test fallisce, dovrebbe individuare la posizione del problema.
    • I test non dovrebbero fare affidamento l'uno sull'altro - Isolati. Nessuna ipotesi sull'ordine di esecuzione del test. Assicurati di "pulire la lavagna" prima di ogni test utilizzando l'impostazione / smontaggio in modo appropriato
  • Professionale : a lungo termine avrai tanto codice di test quanto la produzione (se non di più), quindi segui lo stesso standard di buona progettazione per il tuo codice di test. Metodi ben fattorizzati: classi con nomi che rivelano le intenzioni, nessuna duplicazione, test con buoni nomi, ecc.

  • Anche i buoni test vengono eseguiti velocemente . qualsiasi test che richieda più di mezzo secondo per essere eseguito .. deve essere elaborato. Più tempo impiega la suite di test per una corsa ... meno frequentemente verrà eseguita. Più modifiche lo sviluppatore tenterà di intrufolarsi tra le esecuzioni .. se qualcosa si rompe .. ci vorrà più tempo per capire quale cambiamento è stato il colpevole.

Aggiornamento 2010-08:

  • Leggibile : questo può essere considerato parte di Professional, tuttavia non può essere sottolineato abbastanza. Una prova del fuoco sarebbe trovare qualcuno che non fa parte della tua squadra e chiedergli di capire il comportamento sotto test entro un paio di minuti. I test devono essere mantenuti proprio come il codice di produzione, quindi rendili di facile lettura anche se richiede uno sforzo maggiore. I test dovrebbero essere simmetrici (seguire uno schema) e concisi (testare un comportamento alla volta). Usa una convenzione di denominazione coerente (ad esempio lo stile TestDox). Evita di ingombrare il test con "dettagli accidentali" .. diventa un minimalista.

Oltre a questi, la maggior parte delle altre sono linee guida che riducono il lavoro a basso beneficio: ad es. "Non testare codice che non possiedi" (ad es. DLL di terze parti). Non andare a testare getter e setter. Tieni d'occhio il rapporto costi-benefici o la probabilità di difetti.


Potremmo non essere d'accordo sull'uso di Mocks, ma questo è stato un bel riassunto delle migliori pratiche di unit test.
Justin Standard

Alzerò questa come risposta allora perché trovo utile l'acronimo "A TRIP".
Spoike

3
Sono d'accordo per la maggior parte, ma vorrei sottolineare che c'è un vantaggio nel testare codice che non possiedi ... Stai testando che soddisfi i tuoi requisiti. In quale altro modo puoi essere sicuro che un aggiornamento non danneggerà i tuoi sistemi? (Ma ovviamente, tieni a mente il rapporto costi / benefici quando lo fai.)
Disilluso

@ Craig - Credo che ti riferisci a test di regressione (a livello di interfaccia) (o test degli studenti in alcuni casi), che documentano il comportamento da cui dipendi. Non scriverei test "unitari" per codice di terze parti perché a. il venditore ne sa più di me su quel codice b. Il venditore non è tenuto a preservare alcuna implementazione specifica. Non controllo la modifica a quella base di codice e non voglio sprecare il mio tempo a riparare i test interrotti con un aggiornamento. Quindi preferirei programmare alcuni test di regressione di alto livello per il comportamento che uso (e voglio essere avvisato quando non funziona)
Gishu

@Gishu: Sì, assolutamente! I test devono essere eseguiti solo a livello di interfaccia; e infatti, dovresti al massimo testare le funzionalità che usi effettivamente. Inoltre, quando si sceglie con cosa scrivere questi test; Ho trovato che i semplici framework di test "unitari" di solito si adattano perfettamente al conto.
Disilluso

42
  1. Non scrivere test giganteschi. Come suggerisce l '"unità" in "test unitario", rendi ognuno il più atomico e isolato possibile. Se è necessario, creare le condizioni preliminari utilizzando oggetti fittizi, anziché ricreare manualmente una parte eccessiva dell'ambiente utente tipico.
  2. Non provare cose che ovviamente funzionano. Evita di testare le classi da un fornitore di terze parti, specialmente quello che fornisce le API principali del framework in cui codifichi. Ad esempio, non provare l'aggiunta di un elemento alla classe Hashtable del fornitore.
  3. Prendi in considerazione l'utilizzo di uno strumento di copertura del codice come NCover per scoprire casi limite che devi ancora testare.
  4. Prova a scrivere il test prima dell'implementazione. Pensa al test come più a una specifica a cui la tua implementazione aderirà. Cfr. anche sviluppo guidato dal comportamento, un ramo più specifico dello sviluppo guidato dai test.
  5. Sii coerente. Se scrivi solo test per parte del tuo codice, non è affatto utile. Se lavori in un team e alcuni o tutti gli altri non scrivono test, non è nemmeno molto utile. Convinci te stesso e tutti gli altri dell'importanza (e delle proprietà che fanno risparmiare tempo ) dei test, o non preoccuparti.

1
Buona risposta. Ma non è poi così male se non esegui un test unitario per tutto in una consegna. Certo è preferibile, ma ci devono essere equilibrio e pragmatismo. Oggetto: coinvolgere i tuoi colleghi; a volte basta farlo per dimostrare valore e come punto di riferimento.
Martin Clarke

1
Sono d'accordo. Tuttavia, a lungo termine, è necessario essere in grado di fare affidamento sulla presenza di test, vale a dire in grado di presumere che le insidie ​​comuni verranno colte da loro. In caso contrario, i benefici sono notevolmente ridotti.
Sören Kuklau

2
"Se scrivi solo test per parte del tuo codice, non è affatto utile." È davvero così? Ho progetti con una copertura del codice del 20% (aree cruciali / soggette a errori) e mi hanno aiutato moltissimo, e anche i progetti vanno bene.
dott. male il

1
Sono d'accordo con Slough. Anche se ci sono solo pochi test, dato che sono scritti bene e abbastanza isolati, saranno di grande aiuto.
Spoike

41

La maggior parte delle risposte qui sembra indirizzare le migliori pratiche di unit test in generale (quando, dove, perché e cosa), piuttosto che scrivere effettivamente i test stessi (come). Poiché la domanda sembrava piuttosto specifica sulla parte "come", ho pensato di postarla, presa da una presentazione "borsa marrone" che ho condotto presso la mia azienda.

Le 5 leggi di scrittura di Womp:


1. Utilizzare nomi di metodi di test lunghi e descrittivi.

   - Map_DefaultConstructorShouldCreateEmptyGisMap()
   - ShouldAlwaysDelegateXMLCorrectlyToTheCustomHandlers()
   - Dog_Object_Should_Eat_Homework_Object_When_Hungry()

2. Scrivi i tuoi test in uno stile Arrange / Act / Assert .

  • Sebbene questa strategia organizzativa sia in circolazione da un po 'e abbia chiamato molte cose, l'introduzione dell'acronimo "AAA" di recente è stata un ottimo modo per farlo capire. Rendere tutti i tuoi test coerenti con lo stile AAA li rende facili da leggere e mantenere.

3. Fornisci sempre un messaggio di errore con le tue affermazioni.

Assert.That(x == 2 && y == 2, "An incorrect number of begin/end element 
processing events was raised by the XElementSerializer");
  • Una pratica semplice ma gratificante che rende evidente nella tua applicazione runner cosa ha fallito. Se non fornisci un messaggio, di solito otterrai qualcosa come "Previsto vero, era falso" nell'output di errore, che ti obbliga a leggere il test per scoprire cosa c'è che non va.

4. Commentare il motivo del test : qual è il presupposto aziendale?

  /// A layer cannot be constructed with a null gisLayer, as every function 
  /// in the Layer class assumes that a valid gisLayer is present.
  [Test]
  public void ShouldNotAllowConstructionWithANullGisLayer()
  {
  }
  • Questo può sembrare ovvio, ma questa pratica proteggerà l'integrità dei tuoi test da persone che non capiscono il motivo del test in primo luogo. Ho visto molti test rimossi o modificati che andavano perfettamente bene, semplicemente perché la persona non comprendeva i presupposti che il test stava verificando.
  • Se il test è banale o il nome del metodo è sufficientemente descrittivo, può essere consentito lasciare il commento disattivato.

5. Ogni test deve sempre ripristinare lo stato di ogni risorsa che tocca

  • Usa derisioni dove possibile per evitare di avere a che fare con risorse reali.
  • La pulizia deve essere eseguita a livello di test. I test non devono fare affidamento sull'ordine di esecuzione.

2
+1 a causa del punto 1, 2 e 5 sono importanti. 3 e 4 sembrano piuttosto eccessivi per i test unitari, se si stanno già utilizzando nomi descrittivi dei metodi di test, ma consiglio la documentazione dei test se sono di portata ampia (test funzionale o di accettazione).
Spoike

+1 per conoscenza ed esempi concreti e pratici
Fil

17

Tieni a mente questi obiettivi (adattato dal libro xUnit Test Patterns di Meszaros)

  • I test dovrebbero ridurre il rischio, non introdurlo.
  • I test dovrebbero essere facili da eseguire.
  • I test dovrebbero essere facili da mantenere man mano che il sistema si evolve attorno ad essi

Alcune cose per renderlo più facile:

  • I test dovrebbero fallire solo per un motivo.
  • I test dovrebbero testare solo una cosa
  • Riduci al minimo le dipendenze di test (nessuna dipendenza da database, file, interfaccia utente, ecc.)

Non dimenticare che puoi anche eseguire test di integrazione con il tuo framework xUnit, ma tieni separati i test di integrazione e gli unit test


Immagino volessi dire che hai adattato dal libro "xUnit Test Patterns" di Gerard Meszaros. xunitpatterns.com
Spoike

Sì, hai ragione. Lo
chiarirò

Punti eccellenti. Gli unit test possono essere molto utili ma è molto importante evitare di cadere nella trappola di avere unit test complessi e interdipendenti che creano una tassa enorme per qualsiasi tentativo di cambiare il sistema.
Wedge

9

I test dovrebbero essere isolati. Un test non dovrebbe dipendere da un altro. Inoltre, un test non dovrebbe basarsi su sistemi esterni. In altre parole, prova il tuo codice, non il codice da cui dipende. Puoi testare queste interazioni come parte dei tuoi test di integrazione o funzionali.


9

Alcune proprietà di ottimi unit test:

  • Quando un test fallisce, dovrebbe essere immediatamente ovvio dove risiede il problema. Se devi utilizzare il debugger per rintracciare il problema, i tuoi test non sono sufficientemente granulari. Avere esattamente un'asserzione per test aiuta qui.

  • Quando esegui il refactoring, nessun test dovrebbe fallire.

  • I test dovrebbero essere eseguiti così velocemente che non esiti mai a eseguirli.

  • Tutti i test dovrebbero passare sempre; nessun risultato non deterministico.

  • I test unitari dovrebbero essere ben fattorizzati, proprio come il codice di produzione.

@Alotor: se stai suggerendo che una libreria dovrebbe avere solo unit test nella sua API esterna, non sono d'accordo. Voglio unit test per ogni classe, comprese le classi che non espongo a chiamanti esterni. (Tuttavia, se sento la necessità di scrivere test per metodi privati, allora ho bisogno di refactoring. )


EDIT: C'era un commento sulla duplicazione causata da "un'asserzione per test". In particolare, se si dispone del codice per impostare uno scenario e quindi si desidera fare più affermazioni su di esso, ma si dispone solo di un'asserzione per test, è possibile duplicare l'impostazione su più test.

Non adotto questo approccio. Invece, utilizzo dispositivi di prova per scenario . Ecco un esempio approssimativo:

[TestFixture]
public class StackTests
{
    [TestFixture]
    public class EmptyTests
    {
        Stack<int> _stack;

        [TestSetup]
        public void TestSetup()
        {
            _stack = new Stack<int>();
        }

        [TestMethod]
        [ExpectedException (typeof(Exception))]
        public void PopFails()
        {
            _stack.Pop();
        }

        [TestMethod]
        public void IsEmpty()
        {
            Assert(_stack.IsEmpty());
        }
    }

    [TestFixture]
    public class PushedOneTests
    {
        Stack<int> _stack;

        [TestSetup]
        public void TestSetup()
        {
            _stack = new Stack<int>();
            _stack.Push(7);
        }

        // Tests for one item on the stack...
    }
}

Non sono d'accordo su una sola affermazione per test. Più asserzioni hai in un test, meno casi di test taglia e incolla avrai. Credo che un test case dovrebbe concentrarsi su uno scenario o percorso di codice e le asserzioni dovrebbero derivare da tutti i presupposti e requisiti per soddisfare tale scenario.
Lucas B

Penso che siamo d'accordo che DRY si applica ai test unitari. Come ho detto, "i test unitari dovrebbero essere ben fattorizzati". Tuttavia, esistono diversi modi per risolvere la duplicazione. Uno, come hai detto, è disporre di uno unit test che richiami prima il codice sottoposto a test e quindi lo asserisca più volte. Un'alternativa è creare un nuovo "dispositivo di test" per lo scenario, che richiama il codice sottoposto a test durante un passaggio di inizializzazione / configurazione e quindi ha una serie di test unitari che semplicemente affermano.
Jay Bazuzi

La mia regola pratica è che, se usi il copia-incolla, stai facendo qualcosa di sbagliato. Uno dei miei detti preferiti è "Copia-incolla non è un motivo di design". Sono anche d'accordo che un'asserzione per unit test sia generalmente una buona idea, ma non insisto sempre su di essa. Mi piace il più generale "prova una cosa per test unitario". Anche se questo di solito si traduce in una affermazione per test unitario.
Jon Turner

7

Quello che stai cercando è delineare i comportamenti della classe sottoposta a test.

  1. Verifica dei comportamenti attesi.
  2. Verifica dei casi di errore.
  3. Copertura di tutti i percorsi di codice all'interno della classe.
  4. Esercitare tutte le funzioni membro all'interno della classe.

L'intento di base è aumentare la tua fiducia nel comportamento della classe.

Ciò è particolarmente utile quando si esamina il refactoring del codice. Martin Fowler ha pubblicato un articolo interessante sui test sul suo sito web.

HTH.

Saluti,

rapinare


Rob - meccanico questo va bene, ma manca l'intento. Perché hai fatto tutto questo? Pensare in questo modo può aiutare gli altri lungo il percorso del TDD.
Mark Levison

7

Il test dovrebbe inizialmente fallire. Quindi dovresti scrivere il codice che li fa passare, altrimenti corri il rischio di scrivere un test che è buggato e passa sempre.


@Rismo Non esclusivo di per sé. Per definizione, ciò che Quarrelsome ha scritto qui è esclusivo della metodologia "Test First", che fa parte di TDD. TDD prende in considerazione anche il refactoring. La definizione più "intelligente" che ho letto è che TDD = Test First + Refactoring.
Spoike

Sì, non deve essere TDD, assicurati solo che il tuo test fallisca prima di tutto. Quindi collegare il resto in seguito. Ciò si verifica più comunemente quando si esegue TDD, ma è possibile applicarlo anche quando non si utilizza TDD.
Quibblesome

6

Mi piace l'acronimo Right BICEP dal già citato libro Pragmatic Unit Testing :

  • Giusto : i risultati sono corretti ?
  • B : sono tutti i b condizioni oundary correggere?
  • I : possiamo controllare i nverse relazioni?
  • C : Possiamo c risultati ross-check con altri mezzi?
  • E : Possiamo forzare e condizioni rror per accadere?
  • P : Sono p caratteristiche RESTAZIONI entro limiti?

Personalmente ritengo che si possa andare abbastanza lontano controllando di ottenere i risultati corretti (1 + 1 dovrebbe restituire 2 in una funzione di addizione), provando tutte le condizioni al contorno a cui si può pensare (come usare due numeri di cui la somma è maggiore del valore intero massimo nella funzione di aggiunta) e impone condizioni di errore come errori di rete.


6

I buoni test devono essere mantenibili.

Non ho ancora capito come farlo per ambienti complessi.

Tutti i libri di testo iniziano a scollarsi mentre la tua base di codice inizia a raggiungere le centinaia di migliaia o milioni di righe di codice.

  • Le interazioni di squadra esplodono
  • numero di casi di test esplode
  • le interazioni tra i componenti esplodono.
  • il tempo per costruire tutte le unità diventa una parte significativa del tempo di costruzione
  • una modifica dell'API può propagarsi a centinaia di casi di test. Anche se la modifica del codice di produzione è stata facile.
  • il numero di eventi necessari per sequenziare i processi nello stato corretto aumenta, il che a sua volta aumenta il tempo di esecuzione del test.

Una buona architettura può controllare alcune delle esplosioni di interazione, ma inevitabilmente man mano che i sistemi diventano più complessi, il sistema di test automatizzato cresce con esso.

È qui che inizi a dover affrontare i compromessi:

  • testare solo API esterne altrimenti il ​​refactoring degli interni si traduce in una significativa rielaborazione del caso di test.
  • l'impostazione e lo smontaggio di ogni test diventano più complicati poiché un sottosistema incapsulato mantiene più stato.
  • la compilazione notturna e l'esecuzione automatica dei test crescono fino a poche ore.
  • L'aumento dei tempi di compilazione ed esecuzione significa che i progettisti non eseguiranno o non eseguiranno tutti i test
  • per ridurre i tempi di esecuzione dei test, si consideri la sequenza dei test da adottare per ridurre la configurazione e lo smontaggio

Devi anche decidere:

dove memorizzi i casi di test nella tua base di codice?

  • come documentate i vostri casi di test?
  • le apparecchiature di prova possono essere riutilizzate per salvare la manutenzione del caso di prova?
  • cosa succede quando l'esecuzione di un test case notturno fallisce? Chi fa il triage?
  • Come mantieni gli oggetti fittizi? Se hai 20 moduli che utilizzano tutti il ​​proprio gusto di un'API di registrazione fittizia, la modifica dell'API si increspa rapidamente. Non solo cambiano i casi di test, ma cambiano anche i 20 oggetti fittizi. Quei 20 moduli sono stati scritti nel corso di diversi anni da molti team diversi. È un classico problema di riutilizzo.
  • gli individui e i loro team comprendono il valore dei test automatizzati, semplicemente non gli piace come lo fa l'altro team. :-)

Potrei andare avanti per sempre, ma il punto è che:

I test devono essere mantenibili.


5

Ho trattato questi principi qualche tempo fa in questo articolo di MSDN Magazine che ritengo sia importante da leggere per qualsiasi sviluppatore.

Il modo in cui definisco gli unit test "buoni" è se possiedono le seguenti tre proprietà:

  • Sono leggibili (denominazione, asserzioni, variabili, lunghezza, complessità ..)
  • Sono mantenibili (nessuna logica, non sopra specificata, basata sullo stato, refactored ..)
  • Sono degni di fiducia (prova la cosa giusta, isolata, non test di integrazione ..)

Roy, sono assolutamente d'accordo. Queste cose sono molto più importanti della copertura del caso limite.
Matt Hinze

degno di fiducia - ottimo punto!
ratkok

4
  • Unit Testing verifica solo l'API esterna della tua unità, non dovresti testare il comportamento interno.
  • Ogni test di un TestCase dovrebbe testare un (e solo uno) metodo all'interno di questa API.
    • Casi di test aggiuntivi dovrebbero essere inclusi per i casi di fallimento.
  • Verifica la copertura dei tuoi test: una volta che un'unità è stata testata, il 100% delle linee all'interno di questa unità dovrebbe essere stato eseguito.


1

Non dare mai per scontato che un banale metodo a 2 righe funzioni. Scrivere un rapido unit test è l'unico modo per evitare che il test nullo mancante, il segno meno fuori posto e / o un sottile errore di scoping ti mordano, inevitabilmente quando hai ancora meno tempo per affrontarlo rispetto a adesso.


1

Secondo la risposta "UN VIAGGIO", tranne per il fatto che i test DOVREBBERO fare affidamento l'uno sull'altro !!!

Perché?

DRY - Non ripetere te stesso - vale anche per i test! Le dipendenze del test possono aiutare a 1) risparmiare tempo di configurazione, 2) salvare le risorse del dispositivo e 3) individuare i guasti. Ovviamente, solo dato che il tuo framework di test supporta dipendenze di prima classe. Altrimenti, lo ammetto, sono cattivi.

Segui http://www.iam.unibe.ch/~scg/Research/JExample/


Sono d'accordo con te. TestNG è un altro framework in cui le dipendenze sono consentite facilmente.
Davide

0

Spesso gli unit test si basano su oggetti fittizi o dati fittizi. Mi piace scrivere tre tipi di unit test:

  • unit test "transitori": creano i propri oggetti / dati fittizi e con esso testano la loro funzione, ma distruggono tutto e non lasciano traccia (come nessun dato in un database di test)
  • unit test "persistente": testano le funzioni all'interno del codice creando oggetti / dati che saranno necessari in seguito a funzioni più avanzate per il proprio unit test (evitando che quelle funzioni avanzate ricreano ogni volta il proprio set di oggetti / dati fittizi)
  • unit test "persistenti": unit test che utilizzano oggetti / dati fittizi già presenti (perché creati in un'altra sessione di unit test) dagli unit test persistenti.

Il punto è evitare di riascoltare tutto per poter testare ogni funzione.

  • Eseguo il terzo tipo molto spesso perché tutti gli oggetti / dati fittizi sono già presenti.
  • Eseguo il secondo tipo ogni volta che il mio modello cambia.
  • Eseguo il primo per controllare le funzioni di base di tanto in tanto, per verificare le regressioni di base.

0

Pensa ai 2 tipi di test e trattali in modo diverso: test funzionale e test delle prestazioni.

Utilizza input e metriche diversi per ciascuno. Potrebbe essere necessario utilizzare un software diverso per ogni tipo di test.


Allora per quanto riguarda i test unitari?
Spoike

0

Uso una convenzione di denominazione dei test coerente descritta dagli standard di denominazione dei test unitari di Roy Osherove Ogni metodo in una determinata classe di test case ha il seguente stile di denominazione MethodUnderTest_Scenario_ExpectedResult.

    La prima sezione del nome del test è il nome del metodo nel sistema sottoposto a test.
    Il prossimo è lo scenario specifico che viene testato.
    Infine sono i risultati di quello scenario.

Ogni sezione utilizza Upper Camel Case ed è delimitata da un punteggio inferiore.

L'ho trovato utile quando eseguo il test, i test sono raggruppati in base al nome del metodo in prova. E avere una convenzione consente ad altri sviluppatori di comprendere l'intento del test.

Aggiungo anche parametri al nome del metodo se il metodo sottoposto a test è stato sovraccaricato.

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.