Sta usando i test unitari per raccontare una storia una buona idea?

13

Quindi, ho un modulo di autenticazione che ho scritto qualche tempo fa. Ora vedo gli errori nel mio modo e scrivo i test unitari. Durante la stesura dei test unitari, ho difficoltà a trovare nomi validi 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 difficoltà a distinguere tra i test semplicemente scansionandoli (devo leggere il nome del metodo almeno due volte per sapere cosa è appena fallito)

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

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

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 è sentito parlare di pattern? Ho visto storie di accettazione e simili, ma questo è fondamentalmente diverso. La grande differenza è che sto inventando scenari per "forzare" i test. Piuttosto che cercare manualmente di trovare possibili interazioni che dovrò testare. Inoltre, so che questo incoraggia i test unitari che non testano esattamente un metodo e una classe. Penso che questo sia OK però. Inoltre, sono consapevole che ciò causerà problemi per almeno alcuni framework di test, poiché di solito presuppongono che i test siano indipendenti l'uno dall'altro e che l'ordine non sia importante (dove sarebbe in questo caso).

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

    
posta Earlz 23.01.2013 - 07:47
fonte

3 risposte

15

Sì, è una buona idea dare i nomi dei test degli scenari di esempio che stai testando. E usando il tuo strumento di test unitario per qualcosa di più dei semplici test unitari, 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 lui / lei vuole eseguire, quindi questo non funzionerà più come previsto.

Puoi evitarlo facilmente qui separando la parte di test principale di ogni test (incluso il "assert") dalle parti che hanno impostato 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 asserzione. Quindi riutilizzare questi metodi in diversi test. Dovrai anche aggiungere del codice al metodo [Setup] dei tuoi dispositivi di prova per assicurarti che il sistema si trovi in uno stato iniziale correttamente definito (ad esempio, nessun account finora nel database, nessuno collegato fino ad ora ecc.).

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

Quindi dovrebbe assomigliare a questo:

[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()
    {
        // ...
    }
    // ...
}
    
risposta data 23.01.2013 - 10:48
fonte
4

Un problema nel raccontare una storia con i test unitari è che non rende esplicito che i test unitari dovrebbero essere organizzati ed eseguiti interamente indipendentemente 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.

Questo dà il vantaggio che, oltre a confermare il funzionamento del codice, se un test fallisce, si ottiene la diagnosi di esattamente dove il codice è sbagliato gratuitamente. Se un test non è isolato, devi guardare da cosa dipende per scoprire esattamente cosa è andato storto e perdere un grosso vantaggio dei test unitari. Avere l'ordine di esecuzione può anche sollevare molti falsi negativi, se un test fallisce è possibile che i seguenti test falliscano nonostante il codice testato funzioni perfettamente.

Un buon articolo in modo più approfondito è il classico su test ibridi sporchi .

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

Classe di test:

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 i 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 in errore hanno un nome significativo che ti fornisce una descrizione su esattamente quale metodo ha fallito, la condizione e il risultato atteso.

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

    
risposta data 23.01.2013 - 12:29
fonte
3

Quello che stai descrivendo suona più come Behaviour Driven Design (BDD) rispetto ai test unitari. Dai un'occhiata a SpecFlow che è una tecnologia .NET BDD basata su Gherkin DSL.

Roba potente che ogni essere umano può leggere / scrivere senza sapere nulla sulla codifica. 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 @ @ @ @ @ @ @ sembra solida.

    
risposta data 23.01.2013 - 23:39
fonte

Leggi altre domande sui tag