Come riesci a far funzionare i test in modo efficiente durante la riprogettazione?


14

Una base di codice ben collaudata offre numerosi vantaggi, ma la verifica di alcuni aspetti del sistema comporta una base di codice resistente ad alcuni tipi di modifica.

Un esempio è la verifica di output specifici, ad esempio testo o HTML. I test vengono spesso scritti (ingenuamente?) In modo da prevedere un determinato blocco di testo come output per alcuni parametri di input o per cercare sezioni specifiche in un blocco.

La modifica del comportamento del codice, per soddisfare nuovi requisiti o perché i test di usabilità hanno comportato modifiche all'interfaccia, richiedono anche la modifica dei test, forse anche dei test che non sono specificamente test unitari per il codice che viene modificato.

  • Come gestite il lavoro di ricerca e riscrittura di questi test? E se non potessi semplicemente "eseguirli tutti e lasciare che il framework li risolva"?

  • Quali altri tipi di codice sotto test risultano in test abitualmente fragili?


In che modo è significativamente diverso da programmers.stackexchange.com/questions/5898/… ?
AShelly,

4
Quella domanda erroneamente posta sul refactoring - i test unitari dovrebbero essere invarianti rispetto al refactoring.
Alex Feinman,

Risposte:


9

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

Se impazzisco 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 è stata progettata per essere riutilizzata all'esterno dell'app, questo è semplicemente un inutile sovraccarico di quello che avrebbe potuto essere un rapido cambiamento altrimenti.

Al contrario, se ciò che si sta tentando di cambiare viene esposto o riutilizzato, ognuno di quei test che si dovranno cambiare è la prova di qualcosa che si potrebbe rompere altrove.

In alcuni progetti ciò può equivalere a progettare i test dal livello di accettazione in giù piuttosto che dal test di unità in su. e meno test unitari e più test di stile di integrazione.

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


Penso che volevi scrivere "fuori dal modulo", non "fuori dall'app".
SamB,

SamB, dipende. Se l'interfaccia è interna in alcuni punti all'interno di un'app, ma non pubblica, prenderei in considerazione il test a un livello superiore se pensassi che l'interfaccia sia probabilmente volatile.
Bill

Ho trovato questo approccio molto compatibile con TDD. Mi piace iniziare nei livelli superiori dell'applicazione più vicino all'utente finale in modo da poter progettare i livelli inferiori sapendo come i livelli superiori devono utilizzare i livelli inferiori. Fondamentalmente la costruzione dall'alto verso il basso consente di progettare in modo più accurato l'interfaccia tra un livello e l'altro.
Greg Burghardt,

4

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

In breve, esiste un TIdSipTcpTransport, sottoclasse di TIdSipTransport. Tutti i TIdSipTransports condividono una suite di test comune. All'interno di TIdSipTcpTransport c'erano diverse classi: una mappa che conteneva coppie di connessione / messaggio iniziale, client TCP threaded, un server TCP threaded e così via.

Ecco cosa ho fatto:

  • Eliminate le classi che avrei sostituito.
  • Eliminate le suite di test per quelle classi.
  • Ha lasciato la suite di test specifica per TIdSipTcpTransport (e c'era ancora la suite di test comune a tutti i TIdSipTransports).
  • Esegui i test TIdSipTransport / TIdSipTcpTransport, per assicurarti che tutti abbiano esito negativo.
  • Ha commentato tutti tranne un test TIdSipTransport / TIdSipTcpTransport.
  • Se avessi bisogno di aggiungere una classe, la aggiungerei scrivere test per creare abbastanza funzionalità da superare l'unico test non commentato.
  • Raccogliere, sciacquare, ripetere.

Sapevo quindi 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.

(*) Davvero, non è necessario commentarli. Basta non eseguirli; 100 test falliti non sono molto incoraggianti. Inoltre, nella mia particolare configurazione, compilare meno test significa un ciclo più veloce di test-write-refactor.


L'ho fatto anche alcuni mesi fa e ha funzionato abbastanza bene per me. Tuttavia, non ho potuto assolutamente applicare questo metodo quando mi associo a un collega nella riprogettazione del nostro modulo modello di dominio (che a sua volta ha innescato la riprogettazione di tutti gli altri moduli del progetto).
Marco Ciambrone,

3

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

Puoi:

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

Lo stesso tipo di cose succede con SQL. Se affermi l'effettivo SQL, le tue classi tentano di farti avere problemi. Vuoi davvero affermare i risultati. Quindi uso un database di memoria SQLITE durante i test delle mie unità per assicurarmi che il mio SQL faccia effettivamente ciò che dovrebbe.


Potrebbe anche aiutare a usare HTML strutturale.
SamB,

@SamB sicuramente sarebbe di aiuto, ma non credo che risolverà completamente il problema
Winston Ewert,

certo che no, niente può :-)
SamB,

-1

Innanzitutto crea una NUOVA API, che fa quello che vuoi che sia il tuo NUOVO comportamento API. Se succede che questa nuova API ha lo stesso nome di un'API OLDER, allora aggiungo il nome _NEW al nuovo nome API.

int DoSomethingInterestingAPI ();

diventa:

int DoSomethingInterestingAPI_NEW (int take_more_arguments); int DoSomethingInterestingAPI_OLD (); int DoSomethingInterestingAPI () {DoSomethingInterestingAPI_NEW (whatever_default_mimics_the_old_API); OK - in questa fase - tutti i test di regressione superano il davanzale - usando il nome DoSomethingInterestingAPI ().

SUCCESSIVO, scorrere il codice e modificare tutte le chiamate a DoSomethingInterestingAPI () nella variante appropriata di DoSomethingInterestingAPI_NEW (). Ciò include l'aggiornamento / riscrittura delle parti dei test di regressione che devono essere modificate per utilizzare la nuova API.

SUCCESSIVO, contrassegnare DoSomethingInterestingAPI_OLD () come [[deprecated ()]]. Mantieni l'API obsoleta per tutto il tempo che desideri (fino a quando non avrai aggiornato in modo sicuro tutto il codice che potrebbe dipendere da esso).

Con questo approccio, qualsiasi errore nei test di regressione è semplicemente un bug in quel test di regressione o identifica i bug nel tuo codice, esattamente come vorresti. Questo processo graduale di revisione di un'API mediante la creazione esplicita delle versioni _NEW e _OLD dell'API consente di far coesistere per un po 'parti del vecchio e nuovo codice.

Ecco un buon (difficile) esempio di questo approccio in pratica. Avevo la funzione BitSubstring () - dove avevo usato l'approccio del terzo parametro come COUNT dei bit nella sottostringa. Per essere coerente con altre API e modelli in C ++, volevo passare all'inizio / alla fine come argomenti della funzione.

https://github.com/SophistSolutions/Stroika/commit/003dd8707405c43e735ca71116c773b108c217c0

Ho creato una funzione BitSubstring_NEW con la nuova API e ho aggiornato tutto il mio codice per usarlo (lasciando NESSUNA CHIAMATA a BitSubString). Ma ho lasciato l'implementazione per diverse versioni (mesi) - e l'ho contrassegnato come obsoleto - in modo che tutti potessero passare a BitSubString_NEW (e in quel momento cambiare l'argomento da un conteggio allo stile di inizio / fine).

ALLORA - quando quella transizione è stata completata, ho fatto un altro commit eliminando BitSubString () e rinominando BitSubString_NEW-> BitSubString () (e ho deprecato il nome BitSubString_NEW).


Non aggiungere mai suffissi che non hanno alcun significato o che si autodistruggono dai nomi. Cerca sempre di dare nomi significativi.
Basilevs,

Hai completamente perso il punto. Primo: questi non sono suffissi che "non hanno significato". Hanno il significato che l'API sta passando da una più vecchia a una più recente. In effetti, questo è il punto centrale della DOMANDA a cui stavo rispondendo, e l'intero punto della risposta. I nomi comunicano CHIARAMENTE quale sia l'API VECCHIA, che è la NUOVA API e quale sia il nome finale dell'API una volta completata la transizione. E - i suffissi _OLD / _NEW sono temporanei - SOLO durante la transizione di modifica API.
Lewis Pringle,

Buona fortuna con la versione NEW_NEW_3 dell'API tre anni dopo.
Basilevs,
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.