Metodo di unit test che restituisce una raccolta evitando la logica nel test


14

Sto testando un metodo per generare una raccolta di oggetti dati. Voglio verificare che le proprietà degli oggetti siano impostate correttamente. Alcune proprietà saranno impostate sulla stessa cosa; altri verranno impostati su un valore che dipende dalla loro posizione nella raccolta. Il modo naturale per farlo sembra essere con un ciclo. Tuttavia, Roy Osherove sconsiglia vivamente di usare la logica nei test unitari ( Art of Unit Testing , 178). Lui dice:

Un test che contiene la logica di solito sta testando più di una cosa alla volta, il che non è raccomandato, perché il test è meno leggibile e più fragile. Ma la logica di test aggiunge anche complessità che può contenere un bug nascosto.

I test dovrebbero, come regola generale, essere una serie di chiamate di metodo senza flussi di controllo, nemmeno try-catche con chiamate di asserzione.

Tuttavia, non riesco a vedere nulla di sbagliato nel mio design (in quale altro modo si genera un elenco di oggetti dati, alcuni dei cui valori dipendono da dove si trovano nella sequenza? —Non è possibile generarli e testarli separatamente). C'è qualcosa di non test-friendly con il mio design? O mi sto dedicando troppo rigidamente all'insegnamento di Osherove? O c'è qualche magia segreta di test di unità che non conosco che aggira questo problema? (Sto scrivendo in C # / VS2010 / NUnit, ma se possibile cerco risposte agnostiche).


4
Consiglio di non eseguire il loop. Se il tuo test è che la terza cosa ha la sua barra impostata su Frob, quindi scrivi un test per verificare specificamente che la barra della terza cosa sia Frob. Questo è un test da solo, vai dritto ad esso, nessun loop. Se il tuo test è che ottieni una raccolta di 5 cose, anche questo è un test. Questo non vuol dire che non hai mai un ciclo (esplicito o meno), è solo che spesso non è necessario. Inoltre, tratta il libro di Osherove come più linee guida che regole reali.
Anthony Pegram,

1
I set di @AnthonyPegram non sono ordinati - Frob a volte può essere 3 °, a volte può essere 2 °. Non puoi fare affidamento su di esso, rendendo necessario un ciclo (o funzionalità del linguaggio come Python in), se il test è "Frob è stato aggiunto con successo a una raccolta esistente".
Izkata,

1
@Izbata, la sua domanda menziona specificamente che l'ordinamento è importante. Le sue parole: "gli altri saranno impostati su un valore che dipende dalla loro posizione nella collezione". Esistono molti tipi di raccolte in C # (la lingua a cui fa riferimento) che sono ordinate per l'inserimento. Per questo motivo, puoi anche fare affidamento sull'ordine con le liste in Python, una lingua che menzioni.
Anthony Pegram,

Supponiamo inoltre che tu stia testando un metodo di ripristino su una raccolta. Devi scorrere la collezione e controllare ogni elemento. A seconda delle dimensioni della raccolta, non testarlo in un ciclo è ridicolo. Oppure diciamo che sto testando qualcosa che dovrebbe incrementare ogni elemento in una raccolta. È possibile impostare tutti gli elementi sullo stesso valore, chiamare l'incremento, quindi selezionare. Quel test fa schifo. È necessario impostarne diversi su valori diversi, chiamare l'incremento e verificare che tutti i diversi valori siano stati incrementati correttamente. Controllare un solo oggetto a caso nella collezione sta lasciando molto al caso.
iheanyi,

Non risponderò in questo modo perché otterrò un numero esatto di voti negativi, ma spesso mi limito a toString()raccogliere e confrontare ciò che dovrebbe essere. Semplice e funziona.
user949300,

Risposte:


16

TL; DR:

  • Scrivi il test
  • Se il test fa troppo, anche il codice può fare troppo.
  • Potrebbe non essere un test unitario (ma non un test negativo).

La prima cosa da testare riguarda il dogma che non aiuta. Mi piace leggere The Way of Testivus che evidenzia alcuni problemi con il dogma in modo spensierato.

Scrivi il test che deve essere scritto.

Se il test deve essere scritto in qualche modo, scriverlo in questo modo. Cercare di forzare il test in un layout di test idealizzato o di non averlo affatto non è una buona cosa. Fare un test oggi per testarlo è meglio che fare un test "perfetto" qualche giorno dopo.

Indicherò anche un po 'il brutto test:

Quando il codice è brutto, i test possono essere brutti.

Non ti piace scrivere test brutti, ma il codice brutto deve essere testato di più.

Non lasciare che il codice brutto ti impedisca di scrivere test, ma lascia che il codice brutto ti impedisca di scriverne di più.

Questi possono essere considerati truismi per coloro che seguono da molto tempo ... e si radicano nel modo di pensare e scrivere prove. Per le persone che non sono state e stanno cercando di arrivare a quel punto, i promemoria possono essere utili (trovo persino che rileggerli mi aiuta a evitare di rimanere bloccato in qualche dogma).


Considerare che quando si scrive un brutto test, se il codice può essere un'indicazione che il codice sta provando a fare troppo. Se il codice che stai testando è troppo complesso per essere correttamente esercitato scrivendo un semplice test, potresti prendere in considerazione l'idea di suddividere il codice in parti più piccole che possono essere testate con i test più semplici. Non si dovrebbe scrivere un unit test che fa tutto (potrebbe non essere un unit test allora). Proprio come gli "oggetti di dio" sono cattivi, anche i "test di unità di dio" sono cattivi e dovrebbero essere indicazioni per tornare indietro e rivedere il codice.

Si dovrebbe essere in grado di esercitare tutto il codice con una copertura ragionevole attraverso tali semplici test. I test che fanno più test end-to-end che affrontano domande più grandi ("Ho questo oggetto, eseguito il marshalling in xml, inviato al servizio web, attraverso le regole, il back-out e senza collaudo") è un test eccellente - ma certamente non lo è è un test unitario (e rientra nel regno dei test di integrazione - anche se ha deriso i servizi che chiama e li personalizza nei database di memoria per eseguire i test). Può ancora utilizzare il framework XUnit per i test, ma il framework dei test non lo rende un test unitario.


7

Sto aggiungendo una nuova risposta perché la mia prospettiva è diversa da quando ho scritto la domanda e la risposta originali; non ha senso metterli insieme in uno solo.

Ho detto nella domanda originale

Tuttavia, non riesco a vedere nulla di sbagliato nel mio design (in quale altro modo si genera un elenco di oggetti dati, alcuni dei cui valori dipendono da dove si trovano nella sequenza? —Non è possibile generarli e testarli separatamente)

Questo è dove ho sbagliato. Dopo aver fatto la programmazione funzionale per l'ultimo anno, ora mi rendo conto che avevo solo bisogno di un'operazione di raccolta con un accumulatore. Quindi potrei scrivere la mia funzione come pura funzione che ha funzionato su una cosa e usare alcune funzioni di libreria standard per applicarla alla raccolta.

Quindi la mia nuova risposta è: usa tecniche di programmazione funzionale e eviterai questo problema per la maggior parte del tempo. Puoi scrivere le tue funzioni per operare su singole cose e applicarle solo a raccolte di cose all'ultimo momento. Ma se sono puri puoi testarli senza riferimento alle raccolte.

Per una logica più complessa, fai affidamento su test basati sulle proprietà . Quando hanno una logica, dovrebbe essere inferiore e inversa alla logica del codice in prova, e ogni test verifica molto più di un test unitario basato sul caso che ne vale la pena.

Soprattutto appoggiati sempre ai tuoi tipi . Ottieni i tipi più potenti che puoi e usali a tuo vantaggio. Questo ridurrà il numero di test che devi scrivere in primo luogo.


4

Non provare a provare troppe cose contemporaneamente. Ciascuna delle proprietà di ciascun oggetto dati nella raccolta è troppo per un test. Invece, raccomando:

  1. Se la raccolta è di lunghezza fissa, scrivere un test unitario per convalidare la lunghezza. Se è di lunghezza variabile, scrivere diversi test per lunghezze che ne caratterizzeranno il comportamento (ad es. 0, 1, 3, 10). In entrambi i casi, non convalidare le proprietà in questi test.
  2. Scrivi un unit test per validare ciascuna delle proprietà. Se la raccolta è di lunghezza fissa e corta, basta affermare contro una proprietà di ciascuno degli elementi per ciascun test. Se è di lunghezza fissa ma lunga, scegli un campione rappresentativo ma piccolo degli elementi da asserire contro una proprietà ciascuno. Se è di lunghezza variabile, genera una raccolta relativamente breve ma rappresentativa (cioè forse tre elementi) e asserisci contro una proprietà di ciascuno.

Farlo in questo modo rende i test abbastanza piccoli da non lasciare fastidiosi loop. C # / Esempio di unità, dato metodo sotto test ICollection<Foo> generateFoos(uint numberOfFoos):

[Test]
void generate_zero_foos_returns_empty_list() { ... }
void generate_one_foo_returns_list_of_one_foo() { ... }
void generate_three_foos_returns_list_of_three_foos() { ... }
void generated_foos_have_sequential_ID()
{
    var foos = generateFoos(3).GetEnumerable();
    foos.MoveNext();
    Assert.AreEqual("ID1", foos.Current.id);
    foos.MoveNext();
    Assert.AreEqual("ID2", foos.Current.id);
    foos.MoveNext();
    Assert.AreEqual("ID3", foos.Current.id);
}
void generated_foos_have_bar()
{
    var foos = generateFoos(3).GetEnumerable();
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
    foos.MoveNext();
    Assert.AreEqual("baz", foos.Current.bar);
}

Se sei abituato al paradigma del "test unitario" (nessuna struttura / logica nidificata), questi test sembrano abbastanza chiari. Pertanto, la logica viene evitata nei test identificando il problema originale come il tentativo di testare troppe proprietà contemporaneamente, anziché la mancanza di loop.


1
Osherove avrebbe la testa su un piatto per avere 3 asserzioni. ;) Il primo a fallire significa che non convalidi mai il resto. Nota anche che non hai davvero evitato il ciclo. L'hai appena espanso esplicitamente nella sua forma eseguita. Non è una dura critica, ma solo un suggerimento per ottenere un po 'più di pratica isolando i tuoi casi di test al minimo possibile, per darti un feedback più specifico quando qualcosa fallisce, mentre continui a convalidare altri casi che potrebbero ancora passare (o fallire, con i loro feedback specifici).
Anthony Pegram,

3
@AnthonyPegram Conosco il paradigma one-assert-per-test. Preferisco il mantra "prova una cosa" (come sostenuto da Bob Martin, contro una asserzione per prova, in codice pulito ). Nota a margine: i framework di unit test che hanno "aspettarsi" contro "asserire" sono belli (Google Tests). Per il resto, perché non dividi i tuoi suggerimenti in una risposta completa, con esempi? Penso di poterne beneficiare.
Kazark
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.