Come mantieni efficacemente i tuoi test mentre ridisegni?

14

Un codebase ben collaudato ha una serie di vantaggi, ma testare certi aspetti del sistema si traduce in un codebase resistente a certi tipi di cambiamento.

Un esempio è il test per un output specifico, ad esempio, testo o HTML. I test sono spesso (ingenuamente?) Scritti per prevedere un particolare blocco di testo come output per alcuni parametri di input, o per cercare sezioni specifiche in un blocco.

Cambiando il comportamento del codice, per soddisfare nuovi requisiti o perché i test di usabilità hanno portato a cambiamenti nell'interfaccia, è necessario cambiare anche i test, forse anche i test che non sono specificamente test unitari per il codice che viene modificato.

  • Come gestisci il lavoro di ricerca e riscrittura di questi test? Cosa succede se non puoi semplicemente "eseguirli tutti e lasciare che il framework li risolva"?

  • Quali altri tipi di codice sotto test portano a test abitualmente fragili?

posta Alex Feinman 21.09.2010 - 16:37
fonte

4 risposte

8

So che la gente del TDD odierà questa risposta, ma gran parte di essa per me è scegliere con cura dove testare qualcosa.

Se esagero troppo con i test unitari nei livelli inferiori, non è possibile apportare modifiche significative senza alterare i test unitari. Se l'interfaccia non viene mai esposta e non è pensata per essere riutilizzata all'esterno dell'app, questo è semplicemente inutile a causa di ciò che avrebbe potuto essere una modifica rapida altrimenti.

Viceversa se ciò che stai cercando di cambiare è esposto o riutilizzato, ognuno di quei test che dovrai cambiare è la prova di qualcosa che potresti infrangere altrove.

In alcuni progetti ciò può comportare la progettazione dei test dal livello di accettazione piuttosto che dall'unità di test. e avendo meno test unitari e più test di stile di integrazione.

Ciò non significa che non è ancora possibile identificare una singola funzione e codice finché tale caratteristica non soddisfa i suoi criteri di accettazione. Significa semplicemente che in alcuni casi non si finisce per misurare i criteri di accettazione con i test unitari.

    
risposta data 21.09.2010 - 18:17
fonte
4

Ho appena completato un'importante revisione del mio stack SIP, riscrivendo l'intero trasporto TCP. (Questo era un vicino refactoring, su una scala piuttosto grande, relativa alla maggior parte dei refactoring.)

In breve, c'è una TIdSipTcpTransport, sottoclasse di TIdSipTransport. Tutti i TIdSipTransports condividono una suite di test comune. All'interno di TIdSipTcpTransport c'erano un certo numero di classi: una mappa contenente coppie di connessione / inizializzazione-messaggio, client TCP filettati, un server TCP con thread e così via.

Ecco cosa ho fatto:

  • Cancellati i corsi che stavo per sostituire.
  • Eliminato le suite di test per tali classi.
  • Sinistra la suite di test specifica per TIdSipTcpTransport (e c'era ancora la suite di test comune a tutti TIdSipTransports).
  • Esegui i test TIdSipTransport / TIdSipTcpTransport, per assicurarti che tutti falliscano.
  • Commentato tutto tranne un test TIdSipTransport / TIdSipTcpTransport.
  • Se avessi bisogno di aggiungere una classe, aggiungerei i test di scrittura per creare un numero sufficiente di funzionalità superato dall'unico test non commentato.
  • Metti, risciacqua, ripeti.

Sapevo quindi che cosa dovevo ancora fare, sotto forma di test commentati (*), e sapevo che il nuovo codice funzionava come previsto, grazie ai nuovi test che ho scritto.

(*) In realtà, non è necessario commentarli. Basta non eseguirli; 100 test falliti non sono molto incoraggianti. Inoltre, nel mio particolare setup che compila un numero inferiore di test significa un ciclo più veloce di test-write-refactor.

    
risposta data 21.09.2010 - 18:39
fonte
3

Quando i test sono fragili, lo trovo di solito perché sto testando la cosa sbagliata. Prendiamo ad esempio l'output HTML. Se si controlla l'output HTML effettivo, il test sarà fragile. Ma non ti interessa l'output effettivo, sei interessato a sapere se trasmette le informazioni che dovrebbe. Sfortunatamente, per farlo è necessario fare affermazioni sul contenuto del cervello dell'utente e quindi non può essere fatto automaticamente.

Puoi:

  • Genera l'HTML come test del fumo per assicurarti che venga effettivamente eseguito
  • Utilizza un sistema di modello, in modo da poter testare il processore del modello e i dati inviati al modello, senza testare effettivamente il modello esatto stesso.

Lo stesso genere di cose accade con SQL. Se asserisci l'effettivo SQL che le tue classi cercano di farti essere nei guai. Vuoi veramente affermare i risultati. Quindi uso un database di memoria SQLITE durante i miei test di unità per assicurarmi che il mio SQL faccia effettivamente quello che dovrebbe.

    
risposta data 21.09.2010 - 19:19
fonte
-1

Crea innanzitutto una NUOVA API, che fa ciò che vuoi che sia il tuo nuovo comportamento dell'API. Se accade che questa nuova API abbia lo stesso nome di un'API OLDER, quindi aggiungo il nome _NEW al nuovo nome dell'API.

int DoSomethingInterestingAPI ();

diventa:

int DoSomethingInterestingAPI_NEW (int takes_more_arguments); int DoSomethingInterestingAPI_OLD (); int DoSomethingInterestingAPI () {DoSomethingInterestingAPI_NEW (whatever_default_mimics_the_old_API); OK - in questa fase - tutti i tuoi test di regressione passano - utilizzando il nome DoSomethingInterestingAPI ().

NEXT, passa attraverso il tuo codice e cambia tutte le chiamate a DoSomethingInterestingAPI () nella variante appropriata di DoSomethingInterestingAPI_NEW (). Ciò include l'aggiornamento / riscrittura di qualsiasi parte dei test di regressione che devono essere modificati per utilizzare la nuova API.

NEXT, contrassegna DoSomethingInterestingAPI_OLD () come [[deprecato ()]]. Mantieni l'API obsoleta per tutto il tempo che desideri (finché non hai aggiornato in modo sicuro tutto il codice che potrebbe dipendere da esso).

Con questo approccio, eventuali errori nei test di regressione sono semplicemente bug in quel test di regressione o identificano bug nel codice, esattamente come si vorrebbe. Questo processo graduale di revisione di un'API creando esplicitamente le versioni _NEW e _OLD dell'API consente di avere bit del codice nuovo e vecchio che coesistono da un po '.

Ecco un buon (duro) esempio di questo approccio nella pratica. Avevo la funzione BitSubstring () - dove avevo usato l'approccio di avere il terzo parametro come COUNT di bit nella sottostringa. Per coerenza con altre API e pattern in C ++, volevo passare all'inizio / fine come argomenti alla funzione.

link

Ho creato una funzione BitSubstring_NEW con la nuova API e aggiornato tutto il mio codice per usarlo (lasciando NON PIÙ CHIAMATE a BitSubString). Ma ho lasciato l'implementazione per diverse versioni (mesi) e l'ho contrassegnata come deprecata, quindi tutti potevano passare a BitSubString_NEW (e in quel momento modificare l'argomento da un conteggio allo stile begin / end).

THEN - quando la transizione è stata completata, ho eseguito un altro commit cancellando BitSubString () e rinominando BitSubString_NEW- > BitSubString () (e ho disapprovato il nome BitSubString_NEW).

    
risposta data 17.07.2018 - 04:02
fonte

Leggi altre domande sui tag