Lottando con dipendenze cicliche nei test unitari


24

Sto provando a praticare TDD, usandolo per sviluppare un semplice come Bit Vector. Mi capita di usare Swift, ma questa è una domanda indipendente dalla lingua.

My BitVectorè un file structche memorizza un singolo UInt64e presenta un'API su di esso che ti consente di trattarlo come una raccolta. I dettagli non contano molto, ma è piuttosto semplice. I 57 bit alti sono bit di memorizzazione e i 6 bit inferiori sono bit di "conteggio", che indicano quanti dei bit di memoria memorizzano effettivamente un valore contenuto.

Finora ho una manciata di funzionalità molto semplici:

  1. Un inizializzatore che costruisce vettori di bit vuoti
  2. Una countproprietà di tipoInt
  3. Una isEmptyproprietà di tipoBool
  4. Un operatore di uguaglianza ( ==). NB: questo è un operatore di uguaglianza di valore simile a quello Object.equals()di Java, non un operatore di uguaglianza di riferimento come ==in Java.

Sto incontrando un sacco di dipendenze cicliche:

  1. Il test unitario che verifica il mio inizializzatore deve verificare che il nuovo costruito BitVector. Può farlo in uno dei 3 modi:

    1. Dai un'occhiata bv.count == 0
    2. Dai un'occhiata bv.isEmpty == true
    3. Controllalo bv == knownEmptyBitVector

    Il metodo 1 si basa count, il metodo 2 si basa isEmpty( su cui si basa count, quindi non ha senso utilizzarlo), il metodo 3 si basa ==. In ogni caso, non posso testare il mio inizializzatore da solo.

  2. Il test countdeve funzionare su qualcosa, che inevitabilmente mette alla prova i miei inizializzatori

  3. L'attuazione di isEmptysi basa sucount

  4. L'attuazione di ==si basa su count.

Sono stato in grado di risolvere parzialmente questo problema introducendo un'API privata che costruisce un BitVectormodello di bit esistente (come aUInt64 ). Ciò mi ha permesso di inizializzare i valori senza testare altri inizializzatori, in modo da poter "fare il boot strap" verso l'alto.

Perché i miei test unitari siano davvero test unitari, mi ritrovo a fare un sacco di hack, il che complica notevolmente il mio prod e test code.

Come risolvi esattamente questo tipo di problemi?


20
Stai prendendo una visione troppo ristretta sul termine "unità". BitVectorè una dimensione dell'unità perfettamente fine per i test unitari e risolve immediatamente i problemi di cui i membri pubblici hanno BitVectorbisogno l'uno per l'altro per effettuare test significativi.
Bart van Ingen Schenau,

Sai troppi dettagli di implementazione in anticipo. Il tuo sviluppo è davvero guidato dai test ?
citato il

@herby No, ecco perché mi sto esercitando. Anche se sembra uno standard davvero irraggiungibile. Non credo di aver mai programmato nulla senza una chiara approssimazione mentale di ciò che l'implementazione comporterà.
Alexander - Ripristina Monica il

@Alexander Dovresti provare a rilassarlo, altrimenti sarà test-first, ma non test-driven. Dì solo vago "Farò un po 'di vettore con un int a 64 bit come backing store" e il gioco è fatto; da quel momento in poi fare TDD refactor rosso-verde uno dopo l'altro. I dettagli di implementazione, così come le API, dovrebbero emergere dal tentativo di eseguire i test (il primo) e dalla scrittura di questi test in primo luogo (il secondo).
citato il

Risposte:


66

Ti preoccupi troppo dei dettagli di implementazione.

Non importa che nell'implementazione corrente , isEmptysi basa su count(o qualsiasi altro rapporto si potrebbe avere): tutto si dovrebbe essere preoccuparsi è l'interfaccia pubblica. Ad esempio, puoi avere tre test:

  • Che ha un oggetto appena inizializzato count == 0 .
  • Che ha un oggetto appena inizializzato isEmpty == true
  • Che un oggetto appena inizializzato è uguale all'oggetto vuoto noto.

Questi sono tutti test validi e diventano particolarmente importanti se decidi di fare il refactoring degli interni della tua classe in modo che isEmptyabbia un'implementazione diversa su cui non fare affidamentocount - fintanto che tutti i test continuano a passare, sai che non hai regredito nulla.

Cose simili si applicano agli altri punti: ricorda di testare l'interfaccia pubblica, non l'implementazione interna. Potresti trovare utile TDD qui, dato che dovresti scrivere i test necessari isEmptyprima di aver scritto qualsiasi implementazione.


6
@Alexander Sembri un uomo che ha bisogno di una chiara definizione di unit testing. Il migliore che conosco viene da Michael Feathers
candied_orange

14
@Alexander stai trattando ogni metodo come un pezzo di codice testabile indipendentemente. Questa è la fonte delle tue difficoltà. Queste difficoltà scompaiono se si verifica l'oggetto nel suo insieme, senza tentare di dividerlo in parti più piccole. Le dipendenze tra gli oggetti non sono confrontabili con le dipendenze tra i metodi.
amon

9
@Alexander "un pezzo di codice" è una misurazione arbitraria. Solo inizializzando una variabile stai usando molti "pezzi di codice". Ciò che conta è che stai testando un'unità comportamentale coesa come definita da te .
Ant P

9
"Da quello che ho letto, ho avuto l'impressione che se rompi solo un pezzo di codice, solo i test unitari direttamente correlati a quel codice dovrebbero fallire." Questa sembra essere una regola molto difficile da seguire. (ad es. se scrivi una classe vettoriale e commetti un errore sul metodo dell'indice, probabilmente avrai tonnellate di rottura su tutto il codice che utilizza quella classe vettoriale)
jhominal

4
@Alexander Inoltre, guarda il modello "Disponi, agisci, asserisci" per i test. Fondamentalmente, si imposta l'oggetto in qualunque stato sia necessario (Arrange), si chiama il metodo che si sta effettivamente testando (Act) e quindi si verifica che il suo stato sia cambiato in base alle proprie aspettative. (Affermare). Le cose che hai impostato ad Arrange sarebbero "prerequisiti" per il test.
GalacticCowboy

5

Come risolvi esattamente questo tipo di problemi?

Rivedi il tuo pensiero su cosa sia un "test unitario".

Un oggetto che gestisce dati mutabili in memoria è fondamentalmente una macchina a stati. Così ogni caso l'uso di valore sta per, come minimo, invocare un metodo per inserire le informazioni in oggetto, e invocare un metodo per leggere una copia delle informazioni fuori dell'oggetto. Nei casi d'uso interessanti, si invocherà anche metodi aggiuntivi che cambiano la struttura dei dati.

In pratica, questo sembra spesso

// GIVEN
obj = new Object(...)

// THEN
assert object.read(...)

o

// GIVEN
obj = new Object(...)

// WHEN
object.change(...)

// THEN
assert object.read(...)

La terminologia "unit test" - beh, ha una lunga storia di non essere molto buona.

Li chiamo unit test, ma non corrispondono molto bene alla definizione accettata di unit test: Kent Beck, Test Driven Development by Example

Kent ha scritto la prima versione di SUnit nel 1994 , il porto per JUnit fu nel 1998, la prima bozza del libro TDD fu all'inizio del 2002. La confusione ebbe molto tempo per diffondersi.

L'idea chiave di questi test (più precisamente chiamati "test del programmatore" o "test degli sviluppatori") è che i test sono isolati l'uno dall'altro. I test non condividono alcuna struttura di dati mutabili, quindi possono essere eseguiti contemporaneamente. Non ci sono preoccupazioni che i test debbano essere eseguiti in un ordine specifico per misurare correttamente la soluzione.

Il caso d'uso principale per questi test è che vengono eseguiti dal programmatore tra le modifiche al proprio codice sorgente. Se si sta eseguendo il protocollo refactor rosso verde, un ROSSO imprevisto indica sempre un errore nell'ultima modifica; ripristini tale modifica, verifica che i test siano VERDI e riprova. Non c'è molto vantaggio nel provare a investire in un progetto in cui ogni possibile bug viene colto da un solo test.

Naturalmente, una fusione introduce un errore, quindi scoprire che l'errore non è più banale. Esistono vari passaggi che è possibile eseguire per garantire che i guasti siano facili da localizzare. Vedere


1

In generale (anche se non si utilizza TDD) si dovrebbe cercare di scrivere test il più possibile fingendo di non sapere come sia implementato.

Se stai effettivamente facendo TDD, questo dovrebbe già essere il caso. I tuoi test sono una specifica eseguibile del programma.

L'aspetto del grafico della chiamata sotto i test è irrilevante, purché i test stessi siano sensibili e ben mantenuti.

Penso che il tuo problema sia la tua comprensione del TDD.

Il mio problema secondo me è che stai "mescolando" i tuoi personaggi TDD. Le persone "test", "code" e "refactor" operano in modo completamente indipendente l'una dall'altra, idealmente. In particolare i tuoi personaggi di codifica e refactoring non hanno alcun obbligo nei confronti dei test se non quello di renderli / mantenerli attivi.

Certo, in linea di principio, sarebbe meglio se tutti i test fossero ortogonali e indipendenti l'uno dall'altro. Ma questo non è un problema delle altre due persone TDD, e sicuramente non è un requisito rigido rigoroso o addirittura necessariamente realistico dei tuoi test. Fondamentalmente: non gettare le tue sensazioni di buon senso sulla qualità del codice per cercare di soddisfare un requisito che nessuno ti sta chiedendo.

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.