Usare i test unitari per raccontare una storia è una buona idea?


13

Quindi, ho un modulo di autenticazione che ho scritto qualche tempo fa. Ora sto vedendo gli errori sulla mia strada e sto scrivendo test unitari per questo. Mentre scrivo unit test, faccio fatica a trovare buoni nomi e buone aree da testare. Ad esempio, ho cose come

  • RequiresLogin_should_redirect_when_not_logged_in
  • RequiresLogin_should_pass_through_when_logged_in
  • Login_should_work_when_given_proper_credentials

Personalmente, penso che sia un po 'brutto, anche se sembra "corretto". Ho anche problemi a distinguere tra i test semplicemente scansionandoli (devo leggere il nome del metodo almeno due volte per sapere cosa è fallito)

Quindi, ho pensato che forse invece di scrivere test che puramente testassero la funzionalità, forse scrivere una serie di test che coprono scenari.

Ad esempio, questo è uno stub di prova che ho ideato:

public class Authentication_Bill
{
    public void Bill_has_no_account() 
    { //assert username "bill" not in UserStore
    }
    public void Bill_attempts_to_post_comment_but_is_redirected_to_login()
    { //Calls RequiredLogin and should redirect to login page
    }
    public void Bill_creates_account()
    { //pretend the login page doubled as registration and he made an account. Add the account here
    }
    public void Bill_logs_in_with_new_account()
    { //Login("bill", "password"). Assert not redirected to login page
    }
    public void Bill_can_now_post_comment()
    { //Calls RequiredLogin, but should not kill request or redirect to login page
    }
}

È questo un sentito parlare di modello? Ho visto storie di accettazione e simili, ma questo è fondamentalmente diverso. La grande differenza è che sto inventando scenari per "forzare" i test. Invece di provare manualmente a inventare possibili interazioni che dovrò testare. Inoltre, so che questo incoraggia i test unitari che non testano esattamente un metodo e una classe. Penso che sia OK però. Inoltre, sono consapevole che ciò provocherà problemi per almeno alcuni framework di test, in quanto di solito presuppongono che i test siano indipendenti l'uno dall'altro e l'ordine non importa (dove in questo caso sarebbe).

Ad ogni modo, questo è uno schema consigliabile a tutti? Oppure sarebbe perfetto per i test di integrazione della mia API piuttosto che come test "unitari"? Questo è solo in un progetto personale, quindi sono aperto a esperimenti che possono o non possono andare bene.


4
Le linee tra unità, integrazione e test funzionali sono sfocate, se avessi dovuto scegliere un nome per il test di stub, sarebbe funzionale.
yannis,

Penso che sia una questione di gusti. Personalmente uso il nome di qualunque cosa _testtesterò con allegato e uso i commenti per notare quali risultati mi aspetto. Se è un progetto personale, trova un certo stile con cui ti senti a tuo agio e atteniti a quello.
Lister

1
Ho scritto una risposta con i dettagli su un modo più tradizionale di scrivere unit test usando il modello Arrange / Act / Assert, ma un amico ha avuto molto successo usando github.com/cucumber/cucumber/wiki/Gherkin , che è usato per specifiche e afaik può generare test di cetriolo.
Stuper Utente

Anche se non userei il metodo che hai mostrato con nunit o simili, nspec ha il supporto per costruire il contesto e testare in una natura più guidata dalla storia: nspec.org
Mike

1
cambia "Bill" in "User" e il gioco è fatto
Steven A. Lowe,

Risposte:


15

Sì, è una buona idea assegnare ai tuoi test nomi degli scenari di esempio che stai testando. E usando il tuo strumento di unit test per più di un semplice unit test forse anche ok, molte persone lo fanno con successo (anche io).

Ma no, non è sicuramente una buona idea scrivere i tuoi test in un modo in cui l'ordine di esecuzione dei test è importante. Ad esempio, NUnit consente all'utente di selezionare in modo interattivo quale test desidera eseguire, quindi non funzionerà più come previsto.

Puoi evitarlo facilmente qui separando la parte di test principale di ogni test (incluso "assert") dalle parti che impostano il tuo sistema nello stato iniziale corretto. Usando il tuo esempio sopra: scrivi i metodi per creare un account, accedere e pubblicare un commento - senza alcuna affermazione. Quindi riutilizzare questi metodi in diversi test. Dovrai anche aggiungere un po 'di codice al [Setup]metodo dei tuoi dispositivi di prova per assicurarti che il sistema sia in uno stato iniziale correttamente definito (ad esempio, nessun account finora nel database, nessuno finora collegato ecc.).

EDIT: Naturalmente, questo sembra essere contro la natura "storia" dei tuoi test, ma se dai ai tuoi metodi di aiuto nomi significativi, trovi le tue storie all'interno di ogni test.

Quindi, dovrebbe apparire così:

[TestFixture]
public class Authentication_Bill
{
    [Setup]
    public void Init()
    {  // bring the system in a predefined state, with noone logged in so far
    }

    [Test]
    public void Test_if_Bill_can_create_account()
    {
         CreateAccountForBill();
         // assert that the account was created properly 
    }

    [Test]
    public void Test_if_Bill_can_post_comment_after_login()
    { 
         // here is the "story" now
         CreateAccountForBill();
         LoginWithBillsAccount();
         AddCommentForBill();
        //  assert that the right things happened
    }

    private void CreateAccountForBill()
    {
        // ...
    }
    // ...
}

Andrei oltre e direi che usare uno strumento xUnit per eseguire test funzionali va bene, purché non confondi gli strumenti con il tipo di test e mantieni questi test separati dai test unitari reali, in modo che gli sviluppatori possano esegue comunque test unitari rapidamente al momento del commit. È probabile che questi siano molto più lenti dei test unitari.
bdsl

4

Un problema nel raccontare una storia con test unitari è che non rende esplicito che i test unitari debbano essere organizzati ed eseguiti in modo completamente indipendente l'uno dall'altro.

Un buon test unitario dovrebbe essere completamente isolato da tutti gli altri codici dipendenti, è la più piccola unità di codice che può essere testata.

Ciò offre il vantaggio che oltre a confermare il codice funziona, se un test fallisce si ottiene la diagnosi esattamente dove il codice è sbagliato gratuitamente. Se un test non è isolato devi guardare da cosa dipende per scoprire esattamente cosa è andato storto e perdere uno dei maggiori vantaggi del test unitario. Avere l'ordine di esecuzione della materia può anche sollevare molti falsi negativi, se un test fallisce è possibile che i test seguenti falliscano nonostante il codice che testano funzioni perfettamente.

Un buon articolo più approfondito è il classico sui test ibridi sporchi .

Per rendere leggibili le classi, i metodi e i risultati, il grande test di Art of Unit utilizza la convenzione di denominazione

Classe di prova:

ClassUnderTestTests

Metodi di prova:

MethodUnderTest_Condition_ExpectedResult

Per copiare l'esempio di @Doc Brown, anziché utilizzare [Setup] che viene eseguito prima di ogni test, scrivo metodi di supporto per creare oggetti isolati da testare.

[TestFixture]
public class AuthenticationTests
{
    private Authentication GetAuthenticationUnderTest()
    {
        // create an isolated Authentication object ready for test
    }

    [Test]
    public void CreateAccount_WithValidCredentials_CreatesAccount()
    {
         //Arrange
         Authentication codeUnderTest = GetAuthenticationUnderTest();
         //Act
         Account result = codeUnderTest.CreateAccount("some", "valid", "data");
         //Assert
         //some assert
    }

    [Test]
    public void CreateAccount_WithInvalidCredentials_ThrowsException()
    {
         //Arrange
         Authentication codeUnderTest = GetAuthenticationUnderTest();
         Exception result;
         //Act
         try
         {
             codeUnderTest.CreateAccount("some", "invalid", "data");
         }
         catch(Exception e)
         {
             result = e;
         }
         //Assert
         //some assert
    }
}

Quindi i test falliti hanno un nome significativo che ti dà un po 'di narrativa su quale metodo ha fallito, la condizione e il risultato atteso.

È così che ho sempre scritto unit test, ma un amico ha avuto molto successo con Gerkin .


1
Anche se penso che questo sia un buon post, non sono d'accordo su cosa dice l'articolo collegato sul test "ibrido". Avere "piccoli" test di integrazione (in aggiunta, non in alternativa ai test unitari puri , ovviamente) può essere molto utile, anche se non possono dirti esattamente quale metodo contiene il codice sbagliato. Se tali test sono gestibili dipende da quanto sia pulito il codice per quei test, non sono "sporchi" di per sé. E penso che l'obiettivo di questi test possa essere molto chiaro (come nell'esempio del PO).
Doc Brown,

3

Quello che stai descrivendo suona più come Behavior Driven Design (BDD) che per unità di test per me. Dai un'occhiata a SpecFlow che è una tecnologia .NET BDD basata sul DSL Gherkin .

Roba potente che qualsiasi essere umano può leggere / scrivere senza sapere nulla di programmazione. Il nostro team di test sta riscuotendo un grande successo sfruttandolo per le nostre suite di test di integrazione.

Per quanto riguarda le convenzioni per i test unitari, la risposta di @ DocBrown sembra solida.


Per informazione, BDD è esattamente come TDD, è solo lo stile di scrittura che cambia. Esempio: TDD = assert(value === expected)BDD = value.should.equals(expected)+ descrivi le caratteristiche in strati che risolvono il problema di "indipendenza dai test unitari". Questo è un grande stile!
Offirmo,
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.