Come evitare errori logici nel codice, quando TDD non ha aiutato?


67

Di recente ho scritto un piccolo codice che indicherebbe in modo umano quanti anni ha un evento. Ad esempio, potrebbe indicare che l'evento si è verificato "Tre settimane fa" o "Un mese fa" o "Ieri".

I requisiti erano relativamente chiari e questo era un caso perfetto per lo sviluppo guidato dai test. Ho scritto i test uno per uno, implementando il codice per superare ogni test e tutto sembrava funzionare perfettamente. Fino a quando un bug è apparso in produzione.

Ecco la parte di codice rilevante:

now = datetime.datetime.utcnow()
today = now.date()
if event_date.date() == today:
    return "Today"

yesterday = today - datetime.timedelta(1)
if event_date.date() == yesterday:
    return "Yesterday"

delta = (now - event_date).days

if delta < 7:
    return _number_to_text(delta) + " days ago"

if delta < 30:
    weeks = math.floor(delta / 7)
    if weeks == 1:
        return "A week ago"

    return _number_to_text(weeks) + " weeks ago"

if delta < 365:
    ... # Handle months and years in similar manner.

I test stavano verificando il caso di un evento che si verifica oggi, ieri, quattro giorni fa, due settimane fa, una settimana fa, ecc. E il codice è stato creato di conseguenza.

Quello che mi è mancato è che un evento può accadere un giorno prima, mentre era un giorno fa: per esempio un evento che si è verificato ventisei ore fa sarebbe un giorno fa, mentre non esattamente ieri se ora è l'una. Più esattamente, è un punto qualcosa, ma poiché deltaè un numero intero, sarà solo uno. In questo caso, l'applicazione visualizza "One days ago", che è ovviamente imprevisto e non gestito nel codice. Può essere risolto aggiungendo:

if delta == 1:
    return "A day ago"

subito dopo aver calcolato il delta.

Mentre l'unica conseguenza negativa del bug è che ho perso mezz'ora chiedendomi come potesse accadere questo caso (e credendo che abbia a che fare con i fusi orari, nonostante l'uso uniforme di UTC nel codice), la sua presenza mi preoccupa. Indica che:

  • È molto facile commettere un errore logico anche in un codice sorgente così semplice.
  • Lo sviluppo guidato dai test non ha aiutato.

Inoltre è preoccupante il fatto che non riesco a vedere come si possano evitare tali bug. A parte pensare di più prima di scrivere il codice, l'unico modo in cui riesco a pensare è quello di aggiungere un sacco di asserzioni per i casi che credo non accaderebbero mai (come credevo che un giorno fa fosse necessariamente ieri) e poi scorrere ogni secondo per negli ultimi dieci anni, verificando l'eventuale violazione delle asserzioni, che sembra troppo complessa.

Come potrei evitare di creare questo bug in primo luogo?


38
Avendo un caso di prova per questo? Sembra come l'hai scoperto in seguito e si intreccia con TDD.
Οuroso

63
Hai appena sperimentato perché non sono un fan dello sviluppo guidato dai test - nella mia esperienza la maggior parte dei bug catturati nella produzione sono scenari a cui nessuno ha pensato. Lo sviluppo guidato da test e unit test non fanno nulla per questi. (I test unitari hanno valore nel rilevare i bug introdotti attraverso modifiche future.)
Loren Pechtel,

102
Ripeti dopo di me: "Non ci sono proiettili d'argento, incluso TDD." Non esiste un processo, nessun insieme di regole, nessun algoritmo che puoi seguire roboticamente per produrre un codice perfetto. Se ci fosse, potremmo automatizzare l'intero processo e farcela.
jpmc26,

43
Complimenti, hai riscoperto la vecchia saggezza secondo cui nessun test può dimostrare l'assenza di bug. Ma se stai cercando tecniche per creare una migliore copertura del possibile dominio di input, devi fare un'analisi approfondita del dominio, dei casi limite e delle classi di equivalenza di quel dominio. Tutte le vecchie e ben note tecniche molto conosciute prima dell'invenzione del termine TDD.
Doc Brown,

80
Non sto cercando di essere snarky, ma sembra che la tua domanda possa essere riformulata come "come penso a cose a cui non ho pensato?". Non sono sicuro di cosa abbia a che fare con TDD.
Jared Smith,

Risposte:


57

Questi sono i tipi di errori che si trovano in genere nella fase del refactor di rosso / verde / refactor. Non dimenticare quel passaggio! Considera un refactor come il seguente (non testato):

def pluralize(num, unit):
    if num == 1:
        return unit
    else:
        return unit + "s"

def convert_to_unit(delta, unit):
    factor = 1
    if unit == "week":
        factor = 7 
    elif unit == "month":
        factor = 30
    elif unit == "year":
        factor = 365
    return delta // factor

def best_unit(delta):
    if delta < 7:
        return "day"
    elif delta < 30:
        return "week"
    elif delta < 365:
        return "month"
    else:
        return "year"

def human_friendly(event_date):
    date = event_date.date()
    today = now.date()
    yesterday = today - datetime.timedelta(1)
    if date == today:
        return "Today"
    elif date == yesterday:
        return "Yesterday"
    else:
        delta = (now - event_date).days
        unit = best_unit(delta)
        converted = convert_to_unit(delta, unit)
        pluralized = pluralize(converted, unit)
        return "{} {} ago".format(converted, pluralized)

Qui hai creato 3 funzioni a un livello inferiore di astrazione che sono molto più coerenti e più facili da testare isolatamente. Se avessi lasciato fuori un lasso di tempo che volevi, sarebbe sporgente come un pollice dolorante nelle funzioni di aiuto più semplici. Inoltre, rimuovendo la duplicazione, si riduce il potenziale errore. Dovresti effettivamente aggiungere codice per implementare il tuo caso rotto.

Altri casi di test più sottili vengono anche in mente più prontamente quando si osserva una forma refactored come questa. Ad esempio, cosa dovrebbe best_unitfare se deltaè negativo?

In altre parole, il refactoring non è solo per renderlo carino. Rende più facile per gli umani individuare errori che il compilatore non può.


12
Il prossimo passo è internazionalizzare, e pluralizelavorare solo per un sottoinsieme di parole inglesi sarà una responsabilità.
Deduplicatore

@Deduplicator certo, ma poi a seconda di quali lingue / culture si scelgono come target, si potrebbe cavarsela solo modificando pluralizeusando nume unitcostruendo una chiave di qualche tipo per estrarre una stringa di formato da un file di tabella / risorsa. O potresti aver bisogno di una riscrittura completa della logica, perché hai bisogno di unità diverse ;-)
Hulk,

4
Resta un problema anche con questa rifattorizzazione, che è che "ieri" non ha molto senso nelle prime ore del mattino (poco dopo le 12:01). In termini umani, qualcosa che è accaduto alle 23:59 non cambia improvvisamente da "oggi" a "ieri" quando l'orologio passa oltre la mezzanotte. Invece cambia da "1 minuto fa" a "2 minuti fa". "Oggi" è troppo rozzo in termini di qualcosa che è accaduto ma pochi minuti fa, e "ieri" è pieno di problemi ai nottambuli.
David Hammen,

@DavidHammen Questo è un problema di usabilità e dipende da quanto devi essere preciso. Quando vuoi sapere almeno fino all'ora, non penserei che "ieri" sia buono. "24 ore fa" è molto più chiaro ed è un'espressione umana comunemente usata per enfatizzare il numero di ore. I computer che stanno cercando di essere "amici dell'umanità" quasi sempre sbagliano e lo generalizzano eccessivamente a "ieri", il che è troppo vago. Ma per saperlo dovrai intervistare gli utenti per vedere cosa ne pensano. Per alcune cose vuoi davvero la data e l'ora esatte, quindi "ieri" è sempre sbagliato.
Brandin,

149

Lo sviluppo guidato dai test non ha aiutato.

Sembra che abbia aiutato, è solo che non hai avuto un test per lo scenario "un giorno fa". Presumibilmente, hai aggiunto un test dopo che questo caso è stato trovato; questo è ancora TDD, in quanto quando vengono rilevati dei bug si scrive un unit test per rilevare il bug, quindi risolverlo.

Se dimentichi di scrivere un test per un comportamento, TDD non ha nulla che ti aiuti; ti dimentichi di scrivere il test e quindi non scrivere l'implementazione.


2
Si potrebbe affermare che se lo sviluppatore non avesse usato tdd, sarebbe stato molto più probabile che si sarebbero persi anche altri casi.
Caleb,

75
E per di più, pensa a quanto tempo è stato risparmiato quando stiamo risolvendo il bug? Avendo implementato i test esistenti, hanno capito all'istante che il loro cambiamento non ha rotto il comportamento esistente. Ed erano liberi di aggiungere i nuovi casi di test e refactor senza dover eseguire test manuali approfonditi in seguito.
Caleb,

15
TDD è buono solo come i test scritti.
Mindwin,

Un'altra osservazione: l'aggiunta del test per questo caso migliorerà il design, costringendoci a toglierlo datetime.utcnow()dalla funzione e invece a passare nowcome argomento (riproducibile).
Toby Speight,

114

un evento accaduto ventisei ore fa sarebbe un giorno fa

I test non saranno di grande aiuto se un problema è mal definito. Evidentemente stai mescolando i giorni di calendario con i giorni calcolati in ore. Se rimani fedele ai giorni di calendario, allora alle 01:00, 26 ore fa non è ieri. E se ti attieni alle ore, allora 26 ore fa arriva a 1 giorno fa, indipendentemente dal tempo.


45
Questo è un ottimo punto da sottolineare. Mancare un requisito non significa necessariamente che il processo di implementazione non è riuscito. Significa solo che il requisito non era ben definito. (O hai semplicemente fatto un errore umano, che accadrà di tanto in tanto)
Caleb

Questa è la risposta che volevo fare. Definirei la specifica come "se l'evento fosse questo giorno di calendario, delta presente in ore. Altrimenti usa le date solo per determinare il delta" Le ore di test sono utili solo entro un giorno, se oltre ciò la tua risoluzione è intesa come giorni.
Baldrickk,

1
Mi piace questa risposta perché evidenzia il vero problema: i punti nel tempo e nelle date sono due quantità diverse. Sono collegati ma quando inizi a confrontarli, le cose vanno a sud molto velocemente. Nella programmazione, la logica di data e ora è una delle cose più difficili da ottenere. Non mi piace il fatto che molte implementazioni di date memorizzino sostanzialmente la data come un punto nel tempo 0:00. Fa molta confusione.
Pieter B,

38

Non puoi. TDD è entusiasta di proteggerti da possibili problemi di cui sei a conoscenza. Non aiuta se riscontri problemi che non hai mai considerato. La tua scommessa migliore è che qualcun altro collauda il sistema, potrebbero trovare i casi limite che non hai mai considerato.

Lettura correlata: è possibile raggiungere lo stato di bug assoluto zero per software su larga scala?


2
Avere test scritti da qualcuno diverso dallo sviluppatore è sempre una buona idea, significa che entrambe le parti devono trascurare la stessa condizione di input affinché il bug possa renderlo in produzione.
Michael Kay,

35

Ci sono due approcci che normalmente prendo che trovo possano aiutare.

Innanzitutto, cerco i casi limite. Questi sono luoghi in cui il comportamento cambia. Nel tuo caso, il comportamento cambia in diversi punti lungo la sequenza di giorni interi positivi. C'è un caso limite a zero, a uno, a sette, ecc. Scriverei quindi casi di prova su e intorno ai casi limite. Avrei casi di test a -1 giorni, 0 giorni, 1 ora, 23 ore, 24 ore, 25 ore, 6 giorni, 7 giorni, 8 giorni, ecc.

La seconda cosa che vorrei cercare sono i modelli di comportamento. Nella tua logica per settimane, hai una gestione speciale per una settimana. Probabilmente hai una logica simile in ciascuno degli altri intervalli non mostrati. Questa logica non è presente da giorni, però. Lo guarderei con sospetto fino a quando non avrei potuto spiegare in modo verificabile perché quel caso è diverso, o aggiungerei la logica.


9
Questa è una parte davvero importante del TDD che viene spesso trascurata e raramente ne ho visto parlare in articoli e guide - è davvero importante testare casi limite e condizioni al contorno poiché trovo che sia la fonte del 90% dei bug - of-by -un errore, over e underflow, ultimo giorno del mese, ultimo mese dell'anno, anni bisestili ecc ecc.
GoatInTheMachine,

2
@GoatInTheMachine - e il 90% di questi bug del 90% riguarda transizioni di ora legale ..... Hahaha
Caleb

1
Puoi prima dividere i possibili input in classi di equivalenza e quindi determinare i casi limite ai bordi delle classi. Di certo questo è uno sforzo che può essere più grande dello sforzo di sviluppo; se ne valga la pena dipende da quanto sia importante fornire software il più privo di errori possibile, qual è la scadenza e quanti soldi e pazienza hai.
Peter - Ripristina Monica il

2
Questa è la risposta corretta Molte regole aziendali richiedono di dividere un intervallo di valori in intervalli in cui sono casi da gestire in modi diversi.
abuzittin gillifirca,

14

Non è possibile rilevare errori logici presenti nei requisiti con TDD. Tuttavia, TDD aiuta. Dopotutto, hai trovato l'errore e hai aggiunto un caso di prova. Ma fondamentalmente, TDD garantisce solo che il codice sia conforme al tuo modello mentale. Se il tuo modello mentale è difettoso, i casi di test non li cattureranno.

Ma tieni presente, mentre correggi il bug, i casi di test che avevi già assicurato che nessun comportamento esistente funzionasse correttamente. È abbastanza importante, è facile correggere un bug ma introdurne un altro.

Per trovare in anticipo tali errori, in genere si tenta di utilizzare casi di test basati sulla classe di equivalenza. usando questo principio, sceglieresti un caso per ogni classe di equivalenza e poi tutti i casi limite.

Sceglieresti una data di oggi, ieri, qualche giorno fa, esattamente una settimana fa e diverse settimane fa come esempi da ciascuna classe di equivalenza. Quando esegui il test per le date, devi anche assicurarti che i test non abbiano utilizzato la data di sistema, ma utilizzino una data predeterminata per il confronto. Ciò evidenzierebbe anche alcuni casi limite: ti assicureresti di eseguire i test in qualche momento arbitrario della giornata, lo eseguiresti direttamente dopo mezzanotte, direttamente prima di mezzanotte e persino direttamente a mezzanotte. Ciò significa che per ogni test, ci sarebbero quattro volte di base per cui viene testato.

Quindi aggiungere sistematicamente casi limite a tutte le altre classi. Hai il test per oggi. Quindi aggiungere un tempo appena prima e dopo il comportamento dovrebbe cambiare. Lo stesso per ieri. Lo stesso per una settimana fa ecc.

È probabile che elencando tutti i casi limite in modo sistematico e scrivendo i casi di prova per loro, scopri che le tue specifiche mancano di qualche dettaglio e lo aggiungono. Si noti che la gestione delle date è qualcosa che le persone spesso sbagliano, perché le persone spesso dimenticano di scrivere i loro test in modo che possano essere eseguiti in tempi diversi.

Si noti, tuttavia, che la maggior parte di ciò che ho scritto ha poco a che fare con TDD. Si tratta di scrivere le classi di equivalenza e assicurarsi che le proprie specifiche siano sufficientemente dettagliate su di esse. Questo è il processo con cui minimizzi gli errori logici. TDD si assicura solo che il tuo codice sia conforme al tuo modello mentale.

È difficile trovare casi di test . I test basati sulla classe di equivalenza non sono la fine di tutto e in alcuni casi possono aumentare significativamente il numero di casi di test. Nel mondo reale, l'aggiunta di tutti questi test spesso non è economicamente praticabile (anche se in teoria, dovrebbe essere fatto).


12

L'unico modo in cui riesco a pensare è quello di aggiungere un sacco di asserzioni per i casi che credo non si verificherebbero mai (come credevo che un giorno fa fosse necessariamente ieri) e poi scorrere ogni secondo negli ultimi dieci anni, verificando qualsiasi violazione delle asserzioni, che sembra troppo complessa.

Perchè no? Sembra un'idea abbastanza buona!

L'aggiunta di contratti (asserzioni) al codice è un modo abbastanza solido per migliorarne la correttezza. Generalmente li aggiungiamo come precondizioni all'ingresso della funzione e postcondizioni al ritorno della funzione. Ad esempio, potremmo aggiungere una postcondizione secondo cui tutti i valori restituiti sono nel formato "A [unità] fa" o "[numero] [unità] s fa". Se fatto in modo disciplinato, questo porta alla progettazione per contratto ed è uno dei modi più comuni di scrivere codice ad alta affidabilità.

Criticamente, i contratti non sono destinati a essere testati; sono altrettante specifiche del tuo codice quanto i tuoi test. Tuttavia, è possibile verificare tramite i contratti: chiamare il codice nel test e, se nessuno dei contratti genera errori, il test ha esito positivo. Ripetere ogni secondo degli ultimi dieci anni è un po 'troppo. Ma possiamo sfruttare un altro stile di test chiamato test basato su proprietà .

In PBT invece di testare output specifici del codice, si verifica che l'output obbedisca ad alcune proprietà. Per esempio, una proprietà di una reverse()funzione è che per ogni lista l, reverse(reverse(l)) = l. Il vantaggio di scrivere test come questo è che puoi far sì che il motore PBT generi alcune centinaia di elenchi arbitrari (e alcuni di quelli patologici) e controlla che tutti abbiano questa proprietà. In caso contrario , il motore "riduce" il caso non riuscito per trovare un elenco minimo che rompe il codice. Sembra che tu stia scrivendo Python, che ha l' ipotesi come framework PBT principale.

Quindi, se vuoi un buon modo per trovare casi limite più difficili a cui potresti non pensare, usare contratti e test basati sulla proprietà insieme ti aiuterà molto. Questo non sostituisce i test delle unità di scrittura, ovviamente, ma lo aumenta, il che è davvero il massimo che possiamo fare come ingegneri.


2
Questa è esattamente la soluzione giusta a questo tipo di problema. L'insieme di output validi è facile da definire (è possibile dare un'espressione regolare in modo molto semplice, qualcosa del genere /(today)|(yesterday)|([2-6] days ago)|...) e quindi è possibile eseguire il processo con input selezionati casualmente fino a quando non si trova uno che non è nel set di output previsti. Adottare questo approccio avrebbe rilevato questo errore e non richiederebbe di rendersi conto che l'errore potrebbe esistere in anticipo.
Jules,

@Jules Vedi anche controllo / test delle proprietà . Di solito scrivo test di proprietà durante lo sviluppo, per coprire quanti più casi imprevisti possibile e mi costringono a pensare a proprietà / invarianti generali. Salvo test una tantum per regressioni e simili (di cui il problema dell'autore è un esempio)
Warbo

1
Se esegui così tanti cicli nei test, ci vorrà molto tempo, il che sconfigge uno dei principali obiettivi del test unitario: esegui i test velocemente !
CJ Dennis,

5

Questo è un esempio in cui sarebbe stato utile aggiungere un po 'di modularità. Se un segmento di codice soggetto a errori viene utilizzato più volte, è consigliabile avvolgerlo in una funzione, se possibile.

def time_ago(delta, unit):
    delta_str = _number_to_text(delta) + " " + unit;
    if delta == 1:
        return delta_str + " ago"
    else:
        return delta_str = "s ago"

now = datetime.datetime.utcnow()
today = now.date()
if event_date.date() == today:
    return "Today"

yesterday = today - datetime.timedelta(1)
if event_date.date() == yesterday:
    return "Yesterday"

delta = (now - event_date).days

if delta < 7:
    return time_ago(delta, "day")

if delta < 30:
    weeks = math.floor(delta / 7)
    return time_ago(weeks, "week")

if delta < 365:
    months = math.floor(delta / 31)
    return time_ago(months, "month")

5

Lo sviluppo guidato dai test non ha aiutato.

TDD funziona meglio come tecnica se la persona che scrive i test è contraddittoria. Questo è difficile se non stai programmando una coppia, quindi un altro modo di pensarci è:

  • Non scrivere test per confermare che la funzione sotto test funziona mentre l'hai creata. Scrivi dei test che lo rompono deliberatamente.

Questa è un'arte diversa, che si applica alla scrittura del codice corretto con o senza TDD, e forse forse più complessa (se non di più) della scrittura effettiva del codice. È qualcosa che devi praticare, ed è qualcosa per cui non esiste una risposta unica, facile e semplice.

La tecnica di base per scrivere software robusto, è anche la tecnica di base per capire come scrivere test efficaci:

Comprendi le condizioni preliminari per una funzione - gli stati validi (ovvero quali ipotesi stai facendo sullo stato della classe di cui la funzione è un metodo) e gli intervalli di parametri di input validi - ogni tipo di dati ha un intervallo di valori possibili - un sottoinsieme di cui sarà gestito dalla tua funzione.

Se semplicemente non fai altro che testare esplicitamente questi presupposti sull'inserimento della funzione e assicurarti che una violazione venga registrata o generata e / o che gli errori di funzione vengano eliminati senza ulteriore gestione, puoi rapidamente sapere se il tuo software non riesce nella produzione, rendilo robusto e tollerante agli errori e sviluppa le tue abilità di scrittura di prova contraddittoria.


NB. C'è un'intera letteratura sulle condizioni pre e post, sugli invarianti e così via, insieme alle biblioteche che possono applicarle usando gli attributi. Personalmente non sono un fan di essere così formale, ma vale la pena esaminarlo.


1

Questo è uno dei fatti più importanti sullo sviluppo del software: è assolutamente, assolutamente impossibile scrivere codice privo di bug.

TDD non ti salverà dall'introduzione di bug corrispondenti a casi di test a cui non hai pensato. Inoltre non ti salverà dalla scrittura di un test errato senza accorgertene, quindi dalla scrittura di codice errato che capita di superare il test con errori. E ogni altra singola tecnica di sviluppo software mai creata presenta buchi simili. Come sviluppatori, siamo umani imperfetti. Alla fine della giornata, non c'è modo di scrivere un codice privo di bug al 100%. Non è mai successo e non accadrà mai.

Questo non vuol dire che dovresti rinunciare alla speranza. Sebbene sia impossibile scrivere un codice completamente perfetto, è molto possibile scrivere un codice con così pochi bug che compaiono in casi così rari che il software è estremamente pratico da usare. È praticamente possibile scrivere software che non mostra comportamenti corretti nella pratica .

Ma scriverlo ci impone di abbracciare il fatto che produrremo software difettoso. Quasi tutte le moderne pratiche di sviluppo software sono costruite intorno a un certo livello, sia per impedire che i bug appaiano in primo luogo, sia per proteggerci dalle conseguenze dei bug che inevitabilmente produciamo:

  • La raccolta di requisiti approfonditi ci consente di sapere come appare un comportamento errato nel nostro codice.
  • Scrivere un codice pulito e accuratamente progettato rende più semplice evitare di introdurre bug in primo luogo e più facile correggerli quando li identifichiamo.
  • La scrittura di test ci consente di produrre un registro di ciò che crediamo potrebbe essere uno dei peggiori bug possibili nel nostro software e di dimostrare che almeno tali bug vengono evitati. TDD produce quei test prima del codice, BDD ne ricava i requisiti e i test unitari vecchio stile producono test dopo che il codice è stato scritto, ma tutti prevengono le peggiori regressioni in futuro.
  • Le revisioni tra pari indicano che ogni volta che il codice viene modificato, almeno due paia di occhi hanno visto il codice, diminuendo la frequenza con cui i bug scivolano nel master.
  • L'uso di un tracker di bug o di un tracker di storie utente che tratta i bug come storie degli utenti significa che quando vengono visualizzati i bug, vengono tenuti traccia e alla fine gestiti, non dimenticati e lasciati per entrare costantemente nei modi degli utenti.
  • L'uso di un server di gestione temporanea significa che prima di un'importante versione, tutti i bug dello show-stopper hanno la possibilità di apparire ed essere risolti.
  • L'uso del controllo della versione significa che, nel peggiore dei casi, in cui il codice con bug principali viene inviato ai clienti, è possibile eseguire un rollback di emergenza e ottenere un prodotto affidabile nelle mani dei clienti mentre si risolvono le cose.

La soluzione definitiva al problema che hai identificato non è combattere il fatto che non puoi garantire di scrivere codice privo di bug, ma piuttosto di abbracciarlo. Abbraccia le migliori pratiche del settore in tutte le aree del processo di sviluppo e fornirai costantemente codice ai tuoi utenti che, sebbene non del tutto perfetto, è più che abbastanza robusto per il lavoro.


1

Semplicemente non avevi pensato a questo caso prima e quindi non hai avuto un caso di prova per questo.

Questo succede sempre ed è semplicemente normale. È sempre un compromesso quanto impegno fai nella creazione di tutti i possibili casi di test. Puoi passare un tempo infinito a considerare tutti i casi di test.

Per un pilota automatico di aeromobili passeresti molto più tempo che per un semplice strumento.

Spesso aiuta a pensare agli intervalli validi delle variabili di input e testare questi limiti.

Inoltre, se il tester è una persona diversa rispetto allo sviluppatore, si trovano spesso casi più significativi.


1

(e credendo che abbia a che fare con i fusi orari, nonostante l'uso uniforme di UTC nel codice)

Questo è un altro errore logico nel tuo codice per il quale non hai ancora un test unitario :) - il tuo metodo restituirà risultati errati per gli utenti in fusi orari non UTC. È necessario convertire sia "ora" che la data dell'evento nel fuso orario locale dell'utente prima di eseguire il calcolo.

Esempio: in Australia, un evento si verifica alle 9:00 ora locale. Alle 11 verrà visualizzato come "ieri" perché la data UTC è cambiata.


0
  • Lascia che qualcun altro scriva i test. In questo modo qualcuno che non ha familiarità con l'implementazione potrebbe verificare le situazioni rare a cui non hai mai pensato.

  • Se possibile, iniettare casi di test come raccolte. Questo rende l'aggiunta di un altro test semplice come l'aggiunta di un'altra riga come yield return new TestCase(...). Questo può andare nella direzione dei test esplorativi , automatizzando la creazione di casi di test: "Vediamo cosa restituisce il codice per tutti i secondi di una settimana fa".


0

Sembra che tu abbia l'idea sbagliata che se tutti i tuoi test superano, non hai bug. In realtà, se tutti i test vengono superati, tutto il comportamento noto è corretto. Non sai ancora se il comportamento sconosciuto è corretto o meno.

Spero che tu stia utilizzando la copertura del codice con il tuo TDD. Aggiungi un nuovo test per il comportamento imprevisto. Quindi è possibile eseguire solo il test per il comportamento imprevisto per vedere quale percorso effettivamente prende attraverso il codice. Una volta che conosci il comportamento attuale, puoi apportare una modifica per correggerlo e quando tutti i test passeranno di nuovo, saprai di averlo fatto correttamente.

Questo non significa ancora che il tuo codice sia privo di bug, solo che è meglio di prima e ancora una volta tutto il comportamento noto è corretto!

L'uso corretto di TDD non significa che si scriverà codice privo di bug, significa che si scriveranno meno bug. Tu dici:

I requisiti erano relativamente chiari

Ciò significa che il comportamento più di un giorno ma non ieri è stato specificato nei requisiti? Se hai perso un requisito scritto, è colpa tua. Se hai realizzato che i requisiti erano incompleti mentre lo stavi codificando, buon per te! Se tutti coloro che hanno lavorato ai requisiti hanno perso quel caso, non sei peggio degli altri. Tutti commettono errori, e più sono sottili, più sono facili da perdere. Il grande take away qui è che TDD non impedisce tutti gli errori!


0

È molto facile commettere un errore logico anche in un codice sorgente così semplice.

Sì. Lo sviluppo guidato dai test non cambia questo. È ancora possibile creare bug nel codice effettivo e anche nel codice di test.

Lo sviluppo guidato dai test non ha aiutato.

Oh, ma l'ha fatto! Prima di tutto, quando hai notato il bug, disponevi già dell'intero framework di test e dovevi solo correggere il bug nel test (e il codice effettivo). In secondo luogo, non sai quanti altri bug avresti avuto se non avessi fatto TDD all'inizio.

Inoltre è preoccupante il fatto che non riesco a vedere come si possano evitare tali bug.

Non puoi. Nemmeno la NASA ha trovato il modo di evitare i bug; noi umani inferiori certamente no.

A parte pensare di più prima di scrivere il codice,

Questo è un errore. Uno dei maggiori vantaggi di TDD è che puoi programmare con meno pensieri, perché tutti questi test almeno catturano le regressioni abbastanza bene. Inoltre, anche, o soprattutto con TDD, si non dovrebbe fornire il codice bug-free, in primo luogo (o la vostra velocità di sviluppo semplicemente fermerebbe).

l'unico modo a cui riesco a pensare è quello di aggiungere un sacco di asserzioni per i casi che credo non si verificherebbero mai (come credevo che un giorno fa fosse necessariamente ieri) e poi passare in rassegna ogni secondo negli ultimi dieci anni, controllando qualsiasi violazione delle asserzioni, che sembra troppo complessa.

Ciò sarebbe chiaramente in conflitto con il principio di codificare solo ciò di cui hai effettivamente bisogno in questo momento. Pensavi di aver bisogno di quei casi, e così è stato. Era un pezzo di codice non critico; come hai detto non ci sono stati danni se non te lo sei chiesto per 30 minuti.

Per il codice mission-critical, potresti effettivamente fare quello che hai detto, ma non per il tuo codice standard di tutti i giorni.

Come potrei evitare di creare questo bug in primo luogo?

Non Ti fidi dei tuoi test per trovare la maggior parte delle regressioni; ti attieni al ciclo rosso-verde-refattore, scrivendo i test prima / durante l'effettiva codifica e (importante!) implementi la quantità minima necessaria per effettuare l'interruttore rosso-verde (non più, non meno). Questo finirà con una grande copertura del test, almeno positiva.

Quando, non se, trovi un bug, scrivi un test per riprodurlo e correggi il bug con la minima quantità di lavoro per far passare il test da rosso a verde.


-2

Hai appena scoperto che, indipendentemente da quanto ci provi, non sarai mai in grado di catturare tutti i possibili bug nel tuo codice.

Ciò significa che anche tentare di catturare tutti i bug è un esercizio di futilità, e quindi dovresti usare solo tecniche come TDD come modo per scrivere codice migliore, codice che ha meno bug, non 0 bug.

Ciò a sua volta significa che dovresti dedicare meno tempo all'utilizzo di queste tecniche e spendere quel tempo risparmiato lavorando su modi alternativi per trovare i bug che scivolano attraverso la rete di sviluppo.

alternative come test di integrazione o un team di test, test di sistema, registrazione e analisi di tali log.

Se non riesci a catturare tutti i bug, devi disporre di una strategia per mitigare gli effetti dei bug che ti sfuggono. Se devi farlo comunque, fare più sforzo in questo senso ha più senso che cercare (invano) di fermarli in primo luogo.

Dopo tutto, è inutile spendere una fortuna nel tempo a scrivere test e il primo giorno in cui dai il tuo prodotto a un cliente, cade, in particolare se non hai idea di come trovare e risolvere quel bug. La risoluzione dei bug post mortem e post-consegna è così importante e richiede più attenzione di quanto la maggior parte delle persone spenda nello scrivere test unitari. Salva il test unitario per i bit complicati e non provare la perfezione in anticipo.


Questo è estremamente disfatto. That in turn means you should spend less time using these techniques- ma hai appena detto che aiuterà con meno bug ?!
JᴀʏMᴇᴇ,

@ JᴀʏMᴇᴇ più un'attitudine pragmatica di quale tecnica ti fa guadagnare di più per il tuo buck. Conosco persone che sono orgogliose di passare 10 volte a scrivere test che non sul loro codice, e hanno ancora dei bug. Essere sensibili, piuttosto che dogmatici, sulle tecniche di prova è essenziale. E i test di integrazione devono essere comunque utilizzati, quindi impegnatevi maggiormente in essi che nei test unitari.
gbjbaanb,
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.