Come evitare i test unitari fragili?

24

Abbiamo scritto quasi 3.000 test: i dati sono stati codificati in modo rigido, il riutilizzo del codice è molto scarso. Questa metodologia ha iniziato a mordicchiarci nel culo. Mentre il sistema cambia, ci ritroviamo a passare più tempo a correggere i test non funzionanti. Disponiamo di unità, integrazione e test funzionali.

Quello che sto cercando è un modo definitivo per scrivere test gestibili e mantenibili.

Strutture

posta Chuck Conway 20.09.2011 - 21:05
fonte

7 risposte

21

Non pensarli come "test unitari guasti", perché non lo sono.

Sono specifiche, che il tuo programma non supporta più.

Non pensare a "correggere i test", ma a "definire nuovi requisiti".

I test dovrebbero specificare prima la tua applicazione, non il contrario.

Non puoi dire di avere un'implementazione funzionante finché non sai che funziona. Non puoi dire che funzioni finché non lo test.

Alcune altre note che potrebbero guidarti:

  1. I test e le classi sotto test dovrebbero essere brevi e semplici . Ogni test dovrebbe solo verificare una funzionalità coerente. Cioè, non si preoccupa delle cose che altri test già verificano.
  2. I test e i tuoi oggetti dovrebbero essere accoppiati liberamente, in modo che se si modifica un oggetto, si modifica solo il suo grafico di dipendenza verso il basso e gli altri oggetti che lo utilizzano non ne sono influenzati.
  3. Potresti creare e testare cose sbagliate . I tuoi oggetti sono costruiti per un facile interfacciamento o una facile implementazione? Se è il secondo caso, ti troverai a cambiare molto codice che utilizza l'interfaccia della vecchia implementazione.
  4. Nel migliore dei casi, attenersi rigorosamente al principio di responsabilità unica. Nel caso peggiore, attenersi al principio di Segregazione dell'interfaccia. Vedi Principi SOLID .
risposta data 21.09.2011 - 10:50
fonte
11

Ciò che descrivi potrebbe non essere una cosa così brutta, ma un puntatore a problemi più profondi che i tuoi test scoprono

As the system changes we find ourselves spending more time fixing broken tests. We have unit, integration and functional tests.

Se potessi cambiare il tuo codice e i tuoi test non non si interromperanno, ciò sarebbe sospetto per me. La differenza tra una modifica legittima e un bug è solo il fatto che è richiesto, e ciò che viene richiesto è (presupposto di TDD) definito dai test.

data has been hard coded.

I dati hard coded nei test sono una buona cosa. I test funzionano come falsificazioni, non come prove. Se ci sono troppi calcoli, i test potrebbero essere tautologici. Ad esempio:

assert sum([1,2,3]) == 6
assert sum([1,2,3]) == 1 + 2 + 3
assert sum([1,2,3]) == reduce(operator.add, [1,2,3])

Più è alta l'astrazione, più si avvicina all'algoritmo e più vicino si confronta l'implementazione acuta a se stessa.

very little reuse of code

Il miglior riutilizzo del codice nei test è imho 'Checks', come in jUnits assertThat , perché mantengono i test semplici. Oltre a ciò, se i test possono essere refactoring per condividere il codice, probabilmente il codice testato potrebbe essere , riducendo così i test a quelli che testano la base refactored.

    
risposta data 21.09.2011 - 10:23
fonte
5

Mi sembra che il tuo test unitario funzioni come un incantesimo. È una cosa buona che è così fragile ai cambiamenti, dal momento che è una specie di punto. Piccole modifiche nei test di interruzione di codice in modo da poter eliminare la possibilità di errore in tutto il programma.

Tuttavia, tieni presente che devi solo verificare le condizioni che potrebbero far fallire il tuo metodo o dare risultati imprevisti. Ciò manterrebbe la tua unità test più incline a "rompere" se c'è un vero problema piuttosto che cose banali.

Anche se mi sembra che tu stia ridisegnando pesantemente il programma. In questi casi, fai tutto il necessario e rimuovi i vecchi test e sostituiscili con quelli nuovi in seguito. Riparare i test delle unità è utile solo se non ti aggiusti a causa di cambiamenti radicali nel tuo programma. Altrimenti potresti scoprire che stai dedicando troppo tempo ai test di riscrittura per essere applicabile nella sezione appena scritta del codice del programma.

    
risposta data 21.09.2011 - 10:02
fonte
5

Ho avuto anche questo problema. Il mio approccio migliorato è stato il seguente:

  1. Non scrivere test di unità a meno che non siano l'unico buon modo per testare qualcosa.

    Sono pienamente pronto ad ammettere che i test unitari hanno il costo più basso di diagnosi e tempo per la correzione. Questo li rende uno strumento prezioso. Il problema è che, con l'ovvio differenza del tuo chilometraggio, i test di unità sono spesso troppo meschini per meritare il costo di mantenere la massa del codice. Ho scritto un esempio in fondo, dai uno sguardo.

  2. Usa asserzioni ovunque siano equivalenti al test unitario per quel componente. Le asserzioni hanno la bella proprietà che sono sempre verificate durante ogni build di debug. Quindi, invece di testare i vincoli della classe "Employee" in un'unità separata di test, si sta effettivamente testando la classe Employee attraverso ogni caso di test nel sistema. Le asserzioni hanno anche la proprietà che non lo fanno aumentare la massa del codice tanto quanto i test di unità (che alla fine richiedono impalcature / derisioni / qualsiasi altra cosa).

    Prima che qualcuno mi uccida: le build di produzione non dovrebbero bloccarsi sulle asserzioni. Invece, dovrebbero registrarsi al livello "Errore".

    Come precauzione per qualcuno che non ci ha ancora pensato, non asserire nulla sull'input dell'utente o della rete. È un enorme errore ™.

    Nelle mie ultime basi di codice, ho rimosso con giudizio i test unitari ovunque io veda un'ovvia opportunità per le asserzioni. Ciò ha ridotto in modo significativo i costi di manutenzione complessivi e mi ha reso una persona molto più felice.

  3. Preferisci test di sistema / integrazione, implementandoli per tutti i tuoi flussi primari e le esperienze degli utenti. Probabilmente i casi d'angolo non devono essere qui. Un test di sistema verifica il comportamento a livello utente eseguendo tutti i componenti. Per questo motivo, un test di sistema è necessariamente più lento, quindi scrivi quelli che contano (non di più, niente di meno) e prenderai i problemi più importanti. I test di sistema hanno un sovraccarico di manutenzione molto basso.

    È fondamentale ricordare che, dal momento che stai usando le asserzioni, ogni test del sistema eseguirà un paio di centinaia di "test unitari" allo stesso tempo. Sei anche abbastanza sicuro che i più importanti vengono eseguiti più volte.

  4. Scrivi potenti API che possono essere testate funzionalmente. I test funzionali sono scomodi e (diciamolo) un po 'privi di significato se la tua API rende troppo difficile verificare i componenti funzionanti da soli. La buona progettazione dell'API a) rende le fasi di test semplici e b) genera affermazioni chiare e preziose.

    Il test funzionale è la cosa più difficile da ottenere, specialmente quando si hanno componenti che comunicano uno-a-molti o (anche peggio, oh dio) molti-a-molti attraverso le barriere del processo. Più input e output sono collegati a un singolo componente, più difficile è il test funzionale, perché devi isolarne uno per testarne realmente la funzionalità.

Sulla questione di "non scrivere test di unità", presenterò un esempio:

TEST(exception_thrown_on_null)
{
    InternalDataStructureType sink;
    ASSERT_THROWS(sink.consumeFrom(NULL), std::logic_error);
    try {
        sink.consumeFrom(NULL);
    } catch (const std::logic_error& e) {
        ASSERT(e.what() == "You must not pass NULL as a parameter!");
    }
}

Lo scrittore di questo test ha aggiunto sette righe che non contribuiscono affatto alla verifica del prodotto finale. L'utente dovrebbe mai vedere questo accadendo, sia perché a) nessuno dovrebbe mai passare NULL lì (quindi scrivere un'asserzione, quindi) o b) il caso NULL dovrebbe causare un comportamento diverso. Se il caso è (b), scrivi un test che verifichi effettivamente tale comportamento.

La mia filosofia è diventata che non dovremmo testare gli artefatti di implementazione. Dovremmo testare solo tutto ciò che può essere considerato un risultato reale. Altrimenti, non c'è modo di evitare di scrivere il doppio della massa di codice di base tra i test unitari (che impongono una particolare implementazione) e l'implementazione stessa.

È importante notare, qui, che ci sono buoni candidati per i test unitari. In effetti, ci sono anche diverse situazioni in cui un test unitario è l'unico mezzo adeguato con cui verificare qualcosa e in cui è di alto valore scrivere e mantenere quei test. In cima alla mia testa, questo elenco include algoritmi non banali, contenitori di dati esposti in un'API e codice altamente ottimizzato che appare "complicato" (a.k.a. "il prossimo ragazzo probabilmente lo rovinerà.").

Il mio consiglio specifico a te, quindi: Inizia a eliminare i test unitari in modo giudizioso mentre si rompono, ponendosi la domanda, "è un risultato o sto sprecando codice?" Probabilmente riuscirai a ridurre il numero di cose che ti fanno perdere tempo.

    
risposta data 27.09.2011 - 01:23
fonte
3

Sono sicuro che gli altri avranno molto più input, ma nella mia esperienza, queste sono alcune cose importanti che ti aiuteranno:

  1. Utilizzare una fabbrica di oggetti di prova per costruire strutture di dati di input, quindi non è necessario duplicare quella logica. Forse guarda in una libreria di supporto come AutoFixture per ridurre il codice necessario per l'impostazione di prova.
  2. Per ciascuna classe di test, centralizza la creazione del SUT, quindi sarà facile cambiarlo quando le cose verranno rifatte.
  3. Ricorda che il codice di prova è tanto importante quanto il codice di produzione. Dovrebbe anche essere refactored, se trovi che stai ripetendo te stesso, se il codice si sente irraggiungibile, ecc, ecc.
risposta data 20.09.2011 - 21:10
fonte
2

Gestisci i test come fai con il codice sorgente.

Controllo delle versioni, rilasci dei checkpoint, rilevamento dei problemi, "proprietà delle funzioni", pianificazione e stima degli sforzi, ecc. Lo hanno fatto - penso che questo sia il modo più efficace per affrontare problemi come quelli che descrivi.

    
risposta data 21.09.2011 - 09:28
fonte
1

Dovresti assolutamente dare un'occhiata ai pattern di test XUnit di Gerard Meszaros. Ha una grande sezione con molte ricette per riutilizzare il codice di prova ed evitare la duplicazione.

Se i tuoi test sono fragili, potrebbe anche essere che tu non faccia ricorso a sufficienza per testare il doppio. Soprattutto, se si ricreano interi grafici di oggetti all'inizio di ciascun test di unità, le sezioni di Arrange nei test potrebbero diventare eccessive e spesso potresti trovarti in situazioni in cui devi riscrivere le sezioni di Arrange in un numero considerevole di test solo perché una delle tue classi più usate è cambiata. I mock e gli stub possono aiutarti in questo modo riducendo il numero di oggetti che devi reidratare per avere un contesto di test pertinente.

Prendere i dettagli insignificanti dalle impostazioni di test tramite mock e stub e applicare modelli di test per riutilizzare il codice dovrebbe ridurre significativamente la loro fragilità.

    
risposta data 21.09.2011 - 17:55
fonte

Leggi altre domande sui tag