2010-05-05 25 views
16

Fondamentalmente sto programmando da un po 'e dopo aver finito il mio ultimo progetto posso capire quanto sarebbe stato più facile se avessi fatto TDD. Immagino che non lo sto ancora facendo rigorosamente, mentre sto ancora scrivendo il codice e poi scrivo un test per questo, non capisco come il test diventa prima del codice se non sai quali strutture e come i tuoi dati di archiviazione, ecc. ... ma comunque ...Test unitario - Sto facendo bene?

Tipo di difficile da spiegare, ma fondamentalmente consente di dire ad esempio che ho un oggetto Fruit con proprietà come id, colore e costo. (Tutti memorizzati in file di testo ignorano completamente qualsiasi logica di database, ecc.)

FruitID FruitName FruitColor FruitCost 
    1   Apple  Red   1.2 
    2   Apple  Green  1.4 
    3   Apple  HalfHalf 1.5 

Questo è solo per esempio. Ma diciamo che ho questa è una collezione di oggetti Fruit (è un List<Fruit>) in questa struttura. E la mia logica dirà di riordinare i frutti nella raccolta se un frutto viene eliminato (questo è proprio come deve essere la soluzione).

E.g. se 1 è cancellato, l'oggetto 2 prende la frutta id 1, l'oggetto 3 prende la frutta id2.

Ora voglio testare il codice ive scritta che fa il riordino, ecc

Come posso impostare questa funzione per fare il test?


Ecco dove sono arrivato finora. Fondamentalmente ho classe fruitManager con tutti i metodi, come deletefruit, ecc. Ha la lista di solito ma Ive ha cambiato il metodo hte per testarlo in modo che accetti un elenco e le informazioni sul frutto da eliminare, quindi restituisce l'elenco.

Test delle unità: Sto praticamente facendo questo nel modo giusto o ho un'idea sbagliata? e poi testando l'eliminazione di oggetti/set di dati con valori diversi per garantire che il metodo funzioni correttamente.


[Test] 
public void DeleteFruit() 
{ 
    var fruitList = CreateFruitList(); 
    var fm = new FruitManager(); 

    var resultList = fm.DeleteFruitTest("Apple", 2, fruitList); 

    //Assert that fruitobject with x properties is not in list ? how 
} 

private static List<Fruit> CreateFruitList() 
{ 
    //Build test data 
    var f01 = new Fruit {Name = "Apple",Id = 1, etc...}; 
    var f02 = new Fruit {Name = "Apple",Id = 2, etc...}; 
    var f03 = new Fruit {Name = "Apple",Id = 3, etc...}; 

    var fruitList = new List<Fruit> {f01, f02, f03}; 
    return fruitList; 
} 
+0

Non vorrei riassegnare gli ID se fossi in te – UpTheCreek

+0

per amor di questa domanda, o dire che forse il campo del valore si aggiorna quando un frutto viene cancellato per esempio ... qualcosa del genere – baron

+0

In CreateFruitList(), vorrei ottenere liberare le variabili fXX e aggiungere semplicemente nuovi frutti direttamente alla lista ('fruitList.add (new Fruit (...))'). Solo un piccolo cavillo. –

risposta

12

Se non vedete che cosa prova si dovrebbe iniziare con, è probabilmente che non hai pensato di ciò che la funzionalità dovrebbe fare in termini semplici. Prova ad immaginare un elenco di comportamenti di base che ci si aspetta prioritari.

Qual è la prima cosa che ci si aspetta da un metodo Delete()? Se dovessi spedire il "prodotto" Elimina in 10 minuti, quale sarebbe il comportamento non negoziabile incluso? Beh ... probabilmente che cancella l'elemento.

Quindi:

1) [Test] 
public void Fruit_Is_Removed_From_List_When_Deleted() 

Quando questo test è scritto, passare attraverso l'intero ciclo TDD (eseguire test => rosso, scrivere solo il codice abbastanza per farlo passare => verde; refactoring => verde)

La prossima cosa importante relativa a questo è che il metodo non dovrebbe modificare la lista se il frutto passato come argomento non è nella lista. Così la prossima prova potrebbe essere:

cosa
2) [Test] 
public void Invalid_Fruit_Changes_Nothing_When_Deleted() 

successivo specificato è che gli ID devono essere riorganizzate quando un frutto viene eliminato:

3) [Test] 
public void Fruit_Ids_Are_Reordered_When_Fruit_Is_Deleted() 

cosa mettere in quella prova? Bene, imposta un contesto semplice ma rappresentativo che dimostrerà che il tuo metodo si comporta come previsto.

Ad esempio, creare un elenco di 4 frutti, eliminare il primo e verificare uno ad uno che i 3 frutti rimanenti siano riordinati correttamente. Ciò coprirebbe abbastanza bene lo scenario di base.

Quindi è possibile creare unit test per i casi di errore o borderline:

4) [Test] 
public void Fruit_Ids_Arent_Reordered_When_Last_Fruit_Is_Deleted() 

5) [Test] 
[ExpectedException] 
public void Exception_Is_Thrown_When_Fruit_List_Is_Empty() 

...

7

prima di iniziare a scrivere il tuo primo test, si suppone di avere una vaga idea sulla struttura/design della vostra app, le interfacce ecc La fase di progettazione è spesso una sorta di implicita con TDD .

Immagino che per uno sviluppatore esperto sia ovvio, e leggendo le specifiche di un problema si inizia immediatamente a visualizzare il progetto della soluzione nella sua testa: questo potrebbe essere il motivo per cui è spesso in ordine di scontato. Tuttavia, per uno sviluppatore non esperto, l'attività di progettazione potrebbe dover essere un'impresa più esplicita.

In entrambi i casi, dopo che il primo schizzo del progetto è pronto, TDD può essere utilizzato sia per verificare il comportamento e verificare la solidità/usabilità del design stesso. Potresti iniziare a scrivere il tuo primo test unitario, quindi rendersi conto che "oh, in realtà è piuttosto imbarazzante farlo con l'interfaccia che ho immaginato" - quindi si torna indietro e si riprogetta l'interfaccia. È un approccio iterativo.

Josh Bloch parla di questo in "Coders at Work" - di solito scrive molti casi d'uso per le sue interfacce anche prima di iniziando a implementare qualsiasi cosa. Quindi schizza l'interfaccia, quindi scrive il codice che lo utilizza in tutti i diversi scenari a cui può pensare. Non è ancora compilabile - lo usa semplicemente per capire se la sua interfaccia sta davvero aiutando a realizzare le cose facilmente.

+0

ma dite di averlo e non siete ancora sicuri delle strutture dei dati, ecc. E in che modo riuscirete a guidare l'interfaccia, quindi non sapete come scrivere il test? – baron

+0

Iniziare a dare più senso ma ridisegnare l'interfaccia per motivi di test dell'unità? sembra un po 'un sacrificio da fare? a meno che non lo facciate, è più probabile che produciate un'interfaccia migliore comunque ... – baron

+4

@baron, intendevo esattamente ridisegnare l'interfaccia per renderla migliore in generale. I test unitari sono un cliente specifico in questo senso. Se un'interfaccia è difficile da usare per un test di unità, questo (quasi sempre) significa che è difficile da usare anche per altri client. –

1

Lei non potrà mai essere certi che il vostro unit test copre tutte le eventualità, in modo che sia più o meno la vostra misura personale su come ampiamente si prova e anche ciò che esattamente. Il tuo test unitario dovrebbe almeno testare i casi di confine, che non stai facendo lì. Cosa succede quando provi a eliminare una Apple con un ID non valido? Cosa succede se si dispone di una lista vuota, cosa succede se si elimina il primo/ultimo elemento, ecc.

In generale, non vedo molto il punto nel testare un singolo caso speciale come si fa sopra. Invece Cerco sempre eseguito una serie di test, che nel tuo esempio caso suggerisce un approccio leggermente diverso:

  • In primo luogo, scrivere un metodo di controllo. Puoi farlo non appena sai che avrai una lista di frutti e che in questa lista tutti i frutti avranno ID successivi (è come testare se l'elenco è ordinato). Per questo non deve essere scritto alcun codice per la cancellazione, inoltre è possibile riutilizzarlo in seguito per es. nel codice di inserimento test unitario.

  • Quindi, creare un gruppo di liste diverse (forse casuale) di prova (dimensione vuoto, dimensione media, grande). Anche questo non richiede alcun codice precedente per la cancellazione.

  • Infine, eseguire cancellazioni specifiche per ciascuno degli elenchi di test (eliminare con ID non valido, eliminare ID 1, eliminare l'ultimo id, eliminare ID casuale) e controllare il risultato con il metodo di controllo. A questo punto dovresti almeno conoscere l'interfaccia per il tuo metodo di cancellazione, ma non è necessario che sia già stata scritta.

@Update rispetto al commento: metodo Il correttore è più di un controllo di consistenza in datastructure. Nel tuo esempio, tutti i frutti nella lista hanno ID successivi, quindi viene controllato. Se si dispone di una struttura DAG, è possibile verificarne l'acidità, ecc.

Verificare se l'eliminazione dell'ID x ha funzionato dipende dal fatto che fosse presente nell'elenco o se l'applicazione distingue il caso di un errore. cancellazione dovuta a un ID non valido da uno di successo (in quanto in entrambi i casi non è rimasto alcun ID). Chiaramente, anche voi volete verificare che un ID eliminata non è più presente nella lista è (anche se questo non fa parte di quello che volevo dire con il metodo checker - invece ho pensato abbastanza ovvio omettere).

+0

Eliminando il frutto con fruitid 1 Stavo cercando di testare la cancellazione del primo oggetto. Per quanto riguarda il punto 1 e il metodo di verifica, come funziona esattamente questo per controllare cose come hai menzionato (cancella id non valido, cancella id 1, ultimo id, ecc ...) – baron

1

Dal momento che si sta utilizzando C#, darò per scontato che NUnit è il vostro framework di test. In tal caso, hai a disposizione una serie di affermazioni [..].

Per quanto riguarda le specifiche del codice: non desidero riassegnare gli ID o modificare il make-up degli oggetti Fruit rimanenti in alcun modo quando si manipola l'elenco. Se è necessario l'ID per tenere traccia della posizione dell'oggetto nell'elenco, utilizzare invece .IndexOf().

Con TDD, trovo che scrivere il test prima è spesso un po 'difficile da fare - io alla fine la scrittura del codice prima (codice, o una stringa di hack che è). Un buon trucco è quindi prendere quel "codice" e usarlo come test. Quindi scrivi il tuo codice attuale di nuovo, leggermente diverso. In questo modo avrai due diversi pezzi di codice che realizzeranno la stessa cosa: meno possibilità di commettere lo stesso errore nella produzione e nel codice di prova. Inoltre, dover trovare una seconda soluzione per lo stesso problema potrebbe mostrarti dei punti deboli nell'approccio originale e portare a un codice migliore.

+0

Mi piace che tu menzioni l'uso del primo codice come test codice per scrivere codice migliore per il secondo. Personalmente, di solito riscrivendo le cose per la seconda volta rende sempre più facile leggere e capire il codice. Tuttavia, forse la riassegnazione dell'id era un cattivo esempio, ho appena detto di rendere l'esempio più facile. Per esempio, ad esempio, l'eliminazione di una mela deve comportare il ricalcolo del costo per altre mele, quindi devo modificare il make up degli oggetti frutto del reammining quando manipolo l'elenco. – baron

+1

Preferisco avere test che controllino esattamente una cosa, quindi per un metodo che cancella un elemento da un set Vorrei avere un test per quando l'elemento è nell'insieme insieme ad altri elementi, uno per quando non è in un set non vuoto, uno per quando è l'unico elemento in un set e uno per quando il set è vuoto. –

+0

e anche il tuo modo di dire; e così via come un test per stabilire se il valore di costo ricalcolato correttamente sotto parametri x, sotto y params etc etc? – baron

1
[Test] 
public void DeleteFruit() 
{ 
    var fruitList = CreateFruitList(); 
    var fm = new FruitManager(fruitList); 

    var resultList = fm.DeleteFruit(2); 

    //Assert that fruitobject with x properties is not in list 
    Assert.IsEqual(fruitList[2], fm.Find(2)); 
} 

private static List<Fruit> CreateFruitList() 
{ 
    //Build test data 
    var f01 = new Fruit {Name = "Apple",Id = 1, etc...}; 
    var f02 = new Fruit {Name = "Apple",Id = 2, etc...}; 
    var f03 = new Fruit {Name = "Apple",Id = 3, etc...}; 

    return new List<Fruit> {f01, f02, f03}; 
} 

Si potrebbe provare un po 'di iniezione di dipendenza della lista frutta. L'oggetto fruit manager è un negozio grezzo. Quindi se hai un'operazione di cancellazione hai bisogno di un'operazione di recupero.

Per quanto riguarda il riordino, si desidera che avvenga automaticamente o si desidera un'operazione di resort. Il automaticamente può anche essere eseguito non appena si verifica un'operazione di cancellazione o un pigro solo al momento del recupero. Questo è un dettaglio di implementazione. C'è molto di più che si può dire su questo. Un buon inizio su come gestire un esempio specifico potrebbe essere l'utilizzo di Design by Contract.

[Modifica 1a]

Inoltre si potrebbe prendere in considerazione il motivo per cui il test per le implementazioni specifiche di Fruit. FruitManager dovrebbe gestire un concetto astratto chiamato Fruit. È necessario prestare attenzione a dettagli di implementazione prematuri, a meno che non si desideri utilizzare DTO, ma il problema è che Fruit alla fine potrebbe cambiare da un oggetto con getter a un oggetto con un comportamento effettivo. Ora non solo i tuoi test per Fruit falliscono, ma FruitManager fallirà!

3

Test delle unità: sto praticamente facendo questo nel modo giusto o ho un'idea sbagliata?

Hai perso la barca.

Non riesce quasi mai come il test diventa prima che il codice se non so strutture cosa e come si stanno memorizzando i dati

Questo è il punto Penso che avete bisogno di tornare a, se vuoi che le idee abbiano un senso.

Primo punto: le strutture dati e lo spazio di archiviazione derivano da ciò che è necessario fare il codice, non viceversa. Più in dettaglio, se stai partendo da zero ci sono un numero qualsiasi di implementazioni di strutture/storage che puoi usare; in effetti, dovresti essere in grado di scambiare tra loro senza dover cambiare i test.

Secondo punto: nella maggior parte dei casi, si consuma il codice più spesso di quanto non lo si produca. Lo scrivi una volta, ma tu (e i tuoi colleghi) lo chiami molte volte. Pertanto, la convenienza di chiamare il codice dovrebbe avere una priorità più alta di quella che sarebbe se tu scrivessi la tua soluzione puramente dall'interno.

Così quando ti ritrovi a scrivere un test e scopri che l'implementazione del client è brutta/goffa/inadatta, ti mette in guardia un avviso prima ancora di aver iniziato a implementare qualsiasi cosa. Allo stesso modo, se ti ritrovi a scrivere un sacco di codice di configurazione nei tuoi test, ti dice che non hai davvero le tue preoccupazioni ben separate. Quando ti ritrovi a dire "wow, quel test è stato facile da scrivere", allora probabilmente hai un'interfaccia facile da usare.

È molto difficile raggiungerlo quando si utilizzano esempi orientati all'implementazione (come scrivere un test per un contenitore). Ciò di cui hai bisogno è un problema di giocattoli ben delimitato, indipendente dall'implementazione.

Per un semplice esempio, è possibile prendere in considerazione un gestore di autenticazione - passare un identificatore e un segreto e scoprire se il segreto corrisponde all'identificatore. Quindi dovresti essere in grado di scrivere tre test rapidi in modo corretto: verifica che il segreto corretto consenta l'accesso, verifica che un segreto errato impedisca l'accesso, verifica che quando un segreto viene modificato, solo la nuova versione consente l'accesso.

Quindi è possibile scrivere alcuni semplici test con nomi utente e password. E mentre lo fai, ti rendi conto che i segreti non dovrebbero essere limitati alle stringhe, ma che dovresti essere in grado di rendere segreto qualsiasi cosa serializzabile, e che forse l'accesso non è universale, ma ristretto (questo riguarda il gestore di autenticazione "forse no" e vuoi dimostrare che i segreti sono custoditi in sicurezza ...

È possibile, ovviamente, adottare lo stesso approccio per i container. Ma penso che troverai più facile "ottenerlo" se inizi da un problema utente/aziendale piuttosto che un problema di implementazione.

I test di unità che verificano un'implementazione specifica ("Abbiamo un errore di post-recesso qui?") Hanno un valore. Il processo per crearli è molto più simile a "indovina un bug, scrivi un test per verificare il bug, reagisci se il test fallisce". Questi test tendono a non contribuire al tuo design, tuttavia - è molto più probabile che tu stia clonando un blocco di codice e cambiando alcuni input. Spesso, tuttavia, quando i test unitari seguono l'implementazione, sono spesso difficili da scrivere e hanno notevoli costi di avvio ("perché devo caricare tre librerie e avviare un server Web remoto per verificare un errore di fencepost nel mio ciclo for? ? ").

Letture consigliate Freeman/Pryce, Growing Software Object-Oriented, Guided By Test

1

iniziare con l'interfaccia, hanno una concreta attuazione scheletro.Per ogni metodo/proprietà/evento/costruttore, c'è un comportamento previsto. Inizia con una specifica per il primo comportamento, e completarlo:

[specifica] è stessa [TestFixture] [E] è la stessa [Test]

[Specification] 
When_fruit_manager_has_delete_called_with_existing_fruit : FruitManagerSpecifcation 
{ 
    private IEnumerable<IFruit> _fruits; 

    [It] 
    public void Should_remove_the_expected_fruit() 
    { 
    Assert.Inconclusive("Please implement"); 
    } 

    [It] 
    public void Should_not_remove_any_other_fruit() 
    { 
    Assert.Inconclusive("Please implement"); 
    } 

    [It] 
    public void Should_reorder_the_ids_of_the_remaining_fruit() 
    { 
    Assert.Inconclusive("Please implement"); 
    } 

    /// <summary> 
    /// Setup the SUT before creation 
    /// </summary> 
    public override void GivenThat() 
    { 
    _fruits = new List<IFruit>(); 

    3.Times(_fruits.Add(Mock<IFruit>())); 

    this._fruitToDelete = _fruits[1]; 

    // this fruit is injected in th Sut 
    Dep<IEnumerable<IFruit>>() 
       .Stub(f => ((IEnumerable)f).GetEnumerator()) 
       .Return(this.Fruits.GetEnumerator()) 
       .WhenCalled(mi => mi.ReturnValue = this._fruits.GetEnumerator()); 

    } 

    /// <summary> 
    /// Delete a fruit 
    /// </summary> 
    public override void WhenIRun() 
    { 
    Sut.Delete(this._fruitToDelete); 
    } 
} 

È possibile che questo specifica è solo ad hoc e INCOMPLETO, ma questo è un buon comportamento TDD per approcciare ogni unità/specifica.

qui sarebbe parte del SUT non implementato quando si inizia a lavorare su di esso:

public interface IFruitManager 
{ 
    IEnumerable<IFruit> Fruits { get; } 

    void Delete(IFruit); 
} 

public class FruitManager : IFruitManager 
{ 
    public FruitManager(IEnumerable<IFruit> fruits) 
    { 
    //not implemented 
    } 

    public IEnumerable<IFruit> Fruits { get; private set; } 

    public void Delete(IFruit fruit) 
    { 
    // not implemented 
    } 
} 

Quindi, come potete vedere alcun codice vero e proprio è stato scritto. Se si desidera completare il primo specifico "When _...", è necessario prima eseguire una [ConstructorSpecification] When_fruit_manager_is_injected_with_fruit() perché i frutti iniettati non vengono assegnati alla proprietà Fruits.

Quindi voilà, nessun codice REAL è necessario implementare in un primo momento ... l'unica cosa necessaria ora è la disciplina.

Una cosa che adoro di questo, è che se hai bisogno di classi aggiuntive durante l'implementazione del SUT corrente, non devi implementarle prima di implementare il FruitManager perché puoi semplicemente usare mock come ad esempio ISomeDependencyNeeded ... e quando completi Fruit manager, puoi andare a lavorare sulla classe SomeDependencyNeeded. Piuttosto malvagio.