Come testare quando si organizzano i dati è troppo ingombrante?


19

Sto scrivendo un parser e come parte di ciò, ho una Expanderclasse che "espande" singole istruzioni complesse in più istruzioni semplici. Ad esempio, espanderebbe questo:

x = 2 + 3 * a

in:

tmp1 = 3 * a
x = 2 + tmp1

Ora sto pensando a come testare questa classe, in particolare come organizzare i test. Potrei creare manualmente l'albero della sintassi di input:

var input = new AssignStatement(
    new Variable("x"),
    new BinaryExpression(
        new Constant(2),
        BinaryOperator.Plus,
        new BinaryExpression(new Constant(3), BinaryOperator.Multiply, new Variable("a"))));

Oppure potrei scriverlo come una stringa e analizzarlo:

var input = new Parser().ParseStatement("x = 2 + 3 * a");

La seconda opzione è molto più semplice, più breve e leggibile. Ma introduce anche una negligenza Parser, il che significa che un bug Parserpotrebbe non superare questo test. Quindi, il test smetterebbe di essere un unit test di Expander, e immagino che tecnicamente diventi un test di integrazione di Parsere Expander.

La mia domanda è: va bene affidarsi principalmente (o completamente) a questo tipo di test di integrazione per testare questa Expanderclasse?


3
Che un bug Parserpossa fallire qualche altro test non è un problema se commetti abitualmente solo a zero fallimenti, al contrario significa che hai una copertura maggiore di Parser. Quello di cui preferirei preoccuparmi è che un bug Parserpotrebbe far sì che questo test abbia successo quando avrebbe dovuto fallire . Dopotutto, i test unitari sono alla ricerca di bug: un test viene interrotto quando non lo è, ma dovrebbe esserlo.
Jonas Kölker,

Risposte:


27

Ti ritroverai a scrivere molti più test, con comportamenti molto più complicati, interessanti e utili, se puoi farlo semplicemente. Quindi l'opzione che comporta

var input = new Parser().ParseStatement("x = 2 + 3 * a");

è abbastanza valido. Dipende da un altro componente. Ma tutto dipende da dozzine di altri componenti. Se prendi in giro qualcosa a meno di un centimetro dalla sua vita, probabilmente stai dipendendo da molte funzionalità di derisione e dispositivi di prova.

A volte gli sviluppatori si concentrano eccessivamente sulla purezza dei loro test unitari o sviluppano solo test unitari e test unitari , senza alcun modulo, integrazione, stress o altri tipi di test. Tutti questi moduli sono validi e utili e sono tutti la responsabilità degli sviluppatori, non solo del Q / A o del personale operativo più avanti.

Un approccio che ho usato è quello di iniziare con queste esecuzioni di livello superiore, quindi utilizzare i dati da esse prodotti per costruire l'espressione di test a forma lunga e con il minimo comune comune del test. Ad esempio, quando si esegue il dump della struttura dei dati dal inputprodotto precedente, è possibile creare facilmente:

var input = new AssignStatement(
    new Variable("x"),
    new BinaryExpression(
        new Constant(2),
        BinaryOperator.Plus,
        new BinaryExpression(new Constant(3), BinaryOperator.Multiply, new Variable("a"))));

tipo di test che verifica al livello più basso. In questo modo ottieni un bel mix: una manciata di test primitivi molto elementari (test unitari puri), ma non hai trascorso una settimana a scrivere test a quel livello primitivo. Questo ti dà la risorsa di tempo necessaria per scrivere molti più, leggermente meno test atomici usando Parsercome aiuto. Risultato finale: più test, più copertura, più corner e altri casi interessanti, codice migliore e garanzia di qualità più elevata.


2
Questo è ragionevole, soprattutto per quanto riguarda il fatto che tutto dipende da molti altri. Un buon test unitario dovrebbe testare il minimo possibile. Tutto ciò che rientra in quella quantità minima possibile deve essere testato da un test unitario precedente. Se hai completamente testato Parser, puoi presumere che puoi tranquillamente utilizzare Parser per testare ParseStatement
Jon Story

6
La principale preoccupazione di purezza (penso) è quella di evitare di scrivere dipendenze circolari nei test unitari. Se il test del parser o del parser utilizza l'espansore e questo test di espansione si basa sul funzionamento del parser, allora hai un rischio difficile da gestire che tutto ciò che stai testando è che il parser e l'espansore sono coerenti , mentre cosa che volevi fare era provare che l'espansore effettivamente fa quello che dovrebbe . Ma fintanto che non c'è dipendenza all'indietro, l'uso del parser in questo test unitario non è affatto diverso dall'uso di una libreria standard in un test unitario.
Steve Jessop,

@SteveJessop Ottimo punto. È importante utilizzare componenti indipendenti .
Jonathan Eunice,

3
Qualcosa che ho fatto nei casi in cui il parser stesso è un'operazione costosa (ad esempio leggere i dati da file Excel tramite interoperabilità) è scrivere metodi di generazione di test che eseguono il parser e il codice di output sulla console ricreare la struttura dei dati restituita dal parser . Copio quindi l'output dal generatore in test unitari più convenzionali. Ciò consente di ridurre la dipendenza incrociata in quanto il parser deve funzionare correttamente solo quando i test sono stati creati non ogni volta che vengono eseguiti. (Non perdere qualche secondo / test per creare / distruggere i processi di Excel è stato un bel bonus.)
Dan Neely

+1 per l'approccio di @ DanNeely. Usiamo qualcosa di simile per archiviare diverse versioni serializzate del nostro modello di dati come dati di test, in modo che possiamo essere sicuri che il nuovo codice possa ancora funzionare con dati più vecchi.
Chris Hayes,

6

Certo che va bene!

È sempre necessario un test funzionale / di integrazione che eserciti il ​​percorso completo del codice. E percorso del codice completo in questo caso significa includere la valutazione del codice generato. Cioè test che l'analisi x = 2 + 3 * aproduce codice che se eseguito con a = 5verrà impostato xsu 17e se eseguito con a = -2verrà impostato xsu -4.

Al di sotto di questo, dovresti fare unit test per bit più piccoli purché effettivamente aiuti il ​​debug del codice . I test più precisi che avrai, la maggiore probabilità che qualsiasi modifica al codice debba cambiare anche il test, perché l'interfaccia interna cambia. Tale test ha poco valore a lungo termine e aggiunge lavori di manutenzione. Quindi c'è un punto di rendimenti decrescenti e dovresti fermarti prima.


4

I test unitari ti consentono di individuare elementi specifici che si rompono e dove nel codice si sono rotti. Quindi sono buoni per test a grana molto fine. Buoni test unitari contribuiranno a ridurre i tempi di debug.

Tuttavia, in base alla mia esperienza, i test di unità raramente sono abbastanza buoni da verificare effettivamente il corretto funzionamento. Quindi i test di integrazione sono utili anche per verificare una catena o una sequenza di operazioni. I test di integrazione ti aiutano a superare i test funzionali. Come hai sottolineato, tuttavia, a causa della complessità dei test di integrazione, è più difficile trovare il punto specifico nel codice in cui si interrompe il test. Ha anche un po 'più di fragilità in quanto i guasti in qualsiasi punto della catena causeranno il fallimento del test. Avrai comunque quella catena nel codice di produzione, quindi testare la catena reale è ancora utile.

Idealmente, avresti entrambi, ma in ogni caso, avere un test automatizzato è meglio che non avere test.


0

Esegui molti test sul parser e quando il parser supera i test, salva gli output in un file per deridere il parser e testare l'altro componente.

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.