Il TDD rende ridondante la programmazione difensiva?

103

Oggi ho avuto un'interessante discussione con un collega.

Sono un programmatore difensivo. Credo che la regola " una classe deve garantire che i suoi oggetti abbiano uno stato valido quando si interagisce con all'esterno della classe " deve essere sempre rispettata. Il motivo di questa regola è che la classe non sa chi sono i suoi utenti e che dovrebbe fallire in modo prevedibile quando viene interagita in modo illegale. Secondo me questa regola si applica a tutte le classi.

Nella specifica situazione in cui ho avuto una discussione oggi, ho scritto un codice che convalida che gli argomenti del mio costruttore sono corretti (es. un parametro intero deve essere > 0) e se la precondizione non viene soddisfatta, allora si verifica un'eccezione gettato. D'altro canto, il mio collega crede che tale controllo sia ridondante, perché i test unitari dovrebbero rilevare eventuali usi scorretti della classe. Inoltre, ritiene che anche le convalide di programmazione difensive debbano essere testate su unità, quindi la programmazione difensiva aggiunge molto lavoro e quindi non è ottimale per il TDD.

È vero che TDD è in grado di sostituire la programmazione difensiva? La validazione dei parametri (e non intendo l'input dell'utente) non è necessaria di conseguenza? Oppure le due tecniche si completano a vicenda?

    
posta user2180613 23.09.2016 - 22:21
fonte

14 risposte

195

È ridicolo. TDD forza il codice per superare i test e costringe tutto il codice ad avere alcuni test attorno ad esso. Non impedisce ai tuoi utenti di chiamare in modo errato il codice, né impedisce magicamente ai programmatori di perdere i casi di test.

Nessuna metodologia può costringere gli utenti a usare il codice correttamente.

è una leggera argomentazione da fare che se tu fossi perfettamente TDD avresti preso il tuo > 0 controlla un caso di test, prima di implementarlo, e indirizza questo - probabilmente aggiungendo il controllo. Ma se hai fatto TDD, il tuo requisito (> 0 nel costruttore) dovrebbe prima apparire come un testcase che fallisce. Così facendo il test dopo aver aggiunto il tuo assegno.

È anche ragionevole testare alcune delle condizioni difensive (hai aggiunto la logica, perché non vorresti testare qualcosa di così facilmente testabile?). Non sono sicuro del motivo per cui non sei d'accordo con questo.

Or do the two techniques complement each other?

TDD svilupperà i test. L'implementazione della convalida dei parametri li farà passare.

    
risposta data 23.09.2016 - 22:37
fonte
32

Programmazione difensiva e test di unità sono due modi diversi di catturare errori e ciascuno ha diversi punti di forza. L'utilizzo di un solo modo per rilevare gli errori rende fragili i meccanismi di rilevamento degli errori. L'uso di entrambi catturerà errori che potrebbero essere stati ignorati da uno o l'altro, anche nel codice che non è un'API pubblica; ad esempio, qualcuno potrebbe aver dimenticato di aggiungere un test unitario per dati non validi passati all'API pubblica. Controllare tutto nei luoghi appropriati significa più possibilità di individuare l'errore.

Nella sicurezza delle informazioni, questo è chiamato Defense In Depth. Avere più livelli di difesa garantisce che se uno fallisce, ce ne sono ancora altri da catturarlo.

Il tuo collega ha ragione su una cosa: dovrebbe testare le tue convalide, ma questo non è "lavoro non necessario". È come testare qualsiasi altro codice, vuoi assicurarti che tutti gli usi, anche quelli non validi, abbiano un risultato previsto.

    
risposta data 23.09.2016 - 22:39
fonte
30

TDD non sostituisce assolutamente la programmazione difensiva. Invece, puoi usare TDD per assicurarti che tutte le difese siano a posto e funzioni come previsto.

In TDD, non devi scrivere il codice senza prima scrivere un test - segui il ciclo red-green-refactor religiosamente. Ciò significa che se si desidera aggiungere la convalida, innanzitutto scrivere prima un test che richiede questa convalida. Chiama il metodo in questione con numeri negativi e zero e aspettati di generare un'eccezione.

Inoltre, non dimenticare il passaggio "refactor". Mentre TDD è test- driven , questo non significa test- solo . Dovresti comunque applicare un design adeguato e scrivere codice ragionevole. Scrivere un codice difensivo è un codice ragionevole, perché rende le aspettative più esplicite e il tuo codice nel complesso più solido: individuare gli errori possibili in anticipo facilita il debug.

Ma non dovremmo usare i test per individuare gli errori? Le asserzioni e i test sono complementari. Una buona strategia di test mescolerà vari approcci per assicurarsi che il software sia robusto. Solo i test delle unità o solo i test di integrazione o solo le asserzioni nel codice sono tutti insoddisfacenti, è necessaria una buona combinazione per raggiungere un livello sufficiente di fiducia nel software con uno sforzo accettabile.

Poi c'è un grosso fraintendimento concettuale del tuo collega: i test unitari non possono mai testare usi della tua classe, solo che la classe stessa funziona come previsto in isolamento. Si useranno test di integrazione per verificare che l'interazione tra i vari componenti funzioni, ma l'esplosione combinatoria di possibili casi di test rende impossibile testare tutto. I test di integrazione dovrebbero pertanto limitarsi a un paio di casi importanti. Test più dettagliati che riguardano anche casi limite e casi di errore sono più adatti per i test unitari.

    
risposta data 23.09.2016 - 22:40
fonte
16

I test servono a supportare e garantire la programmazione difensiva

La programmazione difensiva protegge l'integrità del sistema in fase di runtime.

I test sono strumenti di diagnostica (per lo più statici). In fase di esecuzione, i tuoi test non sono in vista. Sono come impalcature usate per montare un alto muro di mattoni o una cupola di roccia. Non lasci parti importanti fuori dalla struttura perché hai un'impalcatura che la sostiene durante la costruzione. Hai un'impalcatura che la sostiene durante la costruzione per facilitare inserendo tutti i pezzi importanti.

MODIFICA: Un'analogia

Che dire di un'analogia con i commenti nel codice?

I commenti hanno il loro scopo, ma possono essere ridondanti o addirittura dannosi. Ad esempio, se metti la conoscenza intrinseca del codice nei commenti , quindi modifica il codice, i commenti diventano irrilevanti e dannosi nel peggiore dei casi.

Quindi affermare di mettere molta conoscenza intrinseca della propria base di codice nei test, come MethodA non può prendere nulla e l'argomento di MethodB deve essere > 0 . Quindi il codice cambia. Null va bene per A ora, e B può assumere valori fino a -10. I test esistenti ora sono funzionalmente sbagliati, ma continueranno a passare.

Sì, dovresti aggiornare i test nello stesso momento in cui aggiorni il codice. Dovresti anche aggiornare (o rimuovere) i commenti nello stesso momento in cui aggiorni il codice. Ma sappiamo tutti che queste cose non sempre accadono e che gli errori sono fatti.

I test verificano il comportamento del sistema. Questo comportamento effettivo è intrinseco al sistema stesso, non intrinseco ai test.

Cosa potrebbe andare storto?

L'obiettivo per quanto riguarda i test è di pensare a tutto ciò che potrebbe andare storto, scrivere un test che controlli il comportamento corretto, quindi creare il codice runtime in modo che passi tutti i test.

Il che significa che la programmazione difensiva è il punto .

Programmazione difensiva TDD , se i test sono completi.

Più test, guidando una programmazione più difensiva

Quando si trovano inevitabilmente bug, vengono scritti più test per modellare le condizioni che manifestano il bug. Quindi il codice è corretto, con il codice che consente di passare i test quelli e i nuovi test rimangono nella suite di test.

Una buona serie di test passerà sia argomenti validi che negativi a una funzione / metodo e si aspettano risultati coerenti. Questo, a sua volta, significa che il componente testato utilizzerà i controlli di precondizione (programmazione difensiva) per confermare gli argomenti passati ad esso.

In generale ...

Ad esempio, se un argomento nullo per una particolare procedura non è valido, almeno un test passerà un valore nullo e ci si aspetta un'eccezione / errore "argomento null non valido" di qualche tipo.

Almeno un altro test sta per passare un argomento valido , ovviamente - o passa attraverso un grande array e passa mille argomenti validi - e conferma che lo stato risultante è appropriato.

Se un test non passa quell'argomento nullo e viene schiaffeggiato con l'eccezione prevista (e quell'eccezione è stata generata perché il codice controllava in modo difensivo lo stato passato ad esso), allora null può terminare assegnato a una proprietà di una classe o sepolto in una raccolta di qualche tipo dove non dovrebbe essere.

Ciò potrebbe causare un comportamento imprevisto in alcune parti del sistema completamente diverse a cui viene passata l'istanza della classe, in alcune localizzazioni geografiche distanti dopo che il software è stato spedito . E questo è il genere di cose che in realtà stiamo cercando di evitare, vero?

Potrebbe anche essere peggio. L'istanza di classe con stato non valido può essere serializzata e archiviata, solo per causare un errore quando viene ricostituito per essere utilizzato in seguito. Geez, non lo so, forse è un sistema di controllo meccanico di qualche tipo che non può riavviarsi dopo un arresto perché non può deserializzare il proprio stato di configurazione persistente. Oppure l'istanza della classe può essere serializzata e passata a un sistema completamente diverso creato da un'altra entità, e il sistema che potrebbe bloccarsi.

Soprattutto se i programmatori di quel altro sistema non codificano in modo difensivo.

    
risposta data 24.09.2016 - 08:54
fonte
9

Al posto di TDD parliamo di "test del software" in generale, e invece di "programmazione difensiva" in generale, parliamo del mio modo preferito di fare programmazione difensiva, che è usando le asserzioni.

Quindi, dato che eseguiamo test del software, dovremmo smettere di inserire dichiarazioni di asserzione nel codice di produzione, giusto? Consentitemi di contare i modi in cui ciò è sbagliato:

  1. Le asserzioni sono opzionali, quindi se non ti piacciono, esegui il tuo sistema con le asserzioni disabilitate.

  2. Le asserzioni controllano cose che il test non può (e non dovrebbe). Perché il testing dovrebbe avere una vista black-box del tuo sistema, mentre le asserzioni hanno una vista white-box. (Certo, dal momento che ci vivono.)

  3. Le asserzioni sono uno strumento di documentazione eccellente. Nessun commento è mai stato, o sarà mai, inequivocabile come un pezzo di codice che asserisce la stessa cosa. Inoltre, la documentazione tende a diventare obsoleta con l'evolversi del codice e non è in alcun modo applicabile dal compilatore.

  4. Le asserzioni possono rilevare errori nel codice di test. Ti sei mai imbattuto in una situazione in cui un test fallisce e non sai chi ha torto: il codice di produzione o il test?

  5. Le asserzioni possono essere più pertinenti del test. I test controlleranno ciò che è prescritto dai requisiti funzionali, ma il codice spesso deve fare alcune ipotesi che sono molto più tecniche di quello. Le persone che scrivono documenti sui requisiti funzionali raramente pensano alla divisione per zero.

  6. Le asserzioni individuano gli errori a cui si riferiscono solo i suggerimenti. Quindi, il tuo test imposta alcune precondizioni estensive, invoca una lunga porzione di codice, raccoglie i risultati e trova che non sono come previsto. Con una sufficiente risoluzione dei problemi, alla fine troverai esattamente dove le cose sono andate storte, ma di solito le asserzioni lo trovano prima.

  7. Le asserzioni riducono la complessità del programma. Ogni singola riga di codice che scrivi aumenta la complessità del programma. Le asserzioni e la parola chiave final ( readonly ) sono gli unici due costrutti che so che riducono effettivamente la complessità del programma. È impagabile.

  8. Le asserzioni aiutano il compilatore a capire meglio il codice. Per favore, prova questo a casa: void foo( Object x ) { assert x != null; if( x == null ) { } } il tuo compilatore dovrebbe emettere un avviso che ti dice che la condizione x == null è sempre falsa. Questo può essere molto utile.

Quanto sopra era un riepilogo di un post dal mio blog, 2014-09-21 "Asserzioni e test "

    
risposta data 23.09.2016 - 23:00
fonte
5

Credo che alla maggior parte delle risposte manchi una distinzione fondamentale: dipende da come verrà utilizzato il codice.

Il modulo in questione verrà utilizzato da altri client indipendenti dall'applicazione che stai testando? Se stai fornendo una libreria o un'API per l'utilizzo da parte di terzi, non hai modo di assicurarti che chiamino il tuo codice solo con un input valido. Devi convalidare tutti gli input.

Ma se il modulo in questione è usato solo dal codice che controlli, allora il tuo amico potrebbe avere un punto. È possibile utilizzare i test unitari per verificare che il modulo in questione sia chiamato solo con un input valido. I controlli di precondizione potrebbero ancora essere considerati una buona pratica, ma è un compromesso: io ti sporchi il codice che controlla la condizione che conosci non può mai sorgere, solo oscura l'intento del codice.

Non sono d'accordo che i controlli di precondizione richiedano più test unitari. Se decidi di non dover testare alcune forme di input non validi, non dovrebbe importare se la funzione contiene o meno controlli di precondizione. Ricorda che i test dovrebbero verificare il comportamento, non i dettagli di implementazione.

    
risposta data 24.09.2016 - 11:46
fonte
3

Questo argomento mi sconcerta, perché quando ho iniziato a praticare TDD, i miei test di unità del modulo "rispondono all'oggetto < certo modo > quando < input non valido >" aumentato 2 o 3 volte. Mi chiedo come faccia il tuo collega a superare con successo quei tipi di test unitari senza che le sue funzioni facciano convalida.

Il caso opposto, che i test unitari mostrano che non si producono mai emittenti cattivi che saranno passati ad argomenti di altre funzioni, è molto più difficile da dimostrare. Come nel primo caso, dipende in larga misura dalla copertura completa dei casi limite, ma è necessario che tutti gli input delle tue funzioni provengano dagli output di altre funzioni di cui hai testato l'unità e non da, ad esempio, input dell'utente o moduli di terze parti.

In altre parole, ciò che fa TDD non ti impedisce di aver bisogno di codice di convalida per quanto ti aiuta a evitare dimenticarlo .

    
risposta data 23.09.2016 - 23:54
fonte
2

Penso di interpretare le osservazioni del collega in modo diverso dalla maggior parte delle altre risposte.

Mi sembra che l'argomento sia:

  • Tutto il nostro codice è stato testato unitamente.
  • Tutto il codice che usa il tuo componente è il nostro codice, o in caso contrario viene testato da qualcun altro (non dichiarato esplicitamente, ma è quello che capisco da "i test unitari dovrebbero rilevare eventuali usi non corretti della classe").
  • Pertanto, per ogni chiamante della tua funzione c'è un test unitario che prende in giro il tuo componente, e il test fallisce se il chiamante passa un valore non valido a quella simulazione.
  • Pertanto, non importa ciò che fa la tua funzione quando viene passato un valore non valido, perché i nostri test dicono che non può accadere.

Per me, questo argomento ha qualche logica, ma fa troppo affidamento sui test unitari per coprire ogni possibile situazione. Il semplice fatto è che il 100% di copertura linea / ramo / percorso non esercita necessariamente ogni valore che il chiamante potrebbe passare, mentre il 100% di copertura di tutti gli stati possibili del chiamante (vale a dire , tutti i possibili valori dei suoi input e variabili) è computazionalmente non fattibile.

Quindi preferirei testare i chiamanti per verificare che (per quanto riguarda i test) non passino mai in valori sbagliati, e in aggiunta per richiedere che il componente non funzioni in alcuni modo riconoscibile quando viene passato un valore negativo (almeno nella misura in cui è possibile riconoscere valori errati nella lingua scelta). Ciò faciliterà il debug quando si verificano problemi nei test di integrazione e aiuterà anche gli utenti della tua classe che sono meno rigorosi nell'isolare la loro unità di codice da quella dipendenza.

Tuttavia, sappi che se documenti e verifichi il comportamento della tua funzione quando viene passato un valore < = 0, allora i valori negativi non sono più non validi (almeno, non di più invalido di qualsiasi argomento a throw , poiché anche questo è documentato per generare un'eccezione!). I chiamanti hanno il diritto di fare affidamento su quel comportamento difensivo. Lingua permettendo, può essere che questo sia in ogni caso lo scenario migliore - la funzione non ha nessun "input non valido", ma i chiamanti che si aspettano di non provocare la funzione nel lancio di un'eccezione dovrebbero essere testato a sufficienza per garantire che non passino i valori che lo provocano.

Pur pensando che il tuo collega sia un po 'meno completamente sbagliato rispetto alla maggior parte delle risposte, raggiungo la stessa conclusione, ovvero che le due tecniche si completano a vicenda. Programma in modo difensivo, documenta i tuoi controlli difensivi e testali. Il lavoro è "non necessario" se gli utenti del tuo codice non possono trarre vantaggio da utili messaggi di errore quando commettono errori. In teoria, se esaminano accuratamente tutto il loro codice prima di integrarlo con il tuo, e non ci sono mai errori nei loro test, allora non vedranno mai i messaggi di errore. In pratica, anche se fanno TDD e iniezione di dipendenza totale, potrebbero comunque essere esplorati durante lo sviluppo o ci potrebbe essere un errore nei loro test. Il risultato è che chiamano il tuo codice prima che il loro codice sia perfetto!

    
risposta data 24.09.2016 - 00:23
fonte
1

Le interfacce pubbliche possono e saranno utilizzate in modo improprio

L'affermazione del tuo collaboratore "i test unitari dovrebbero rilevare eventuali usi scorretti della classe" è rigorosamente falsa per qualsiasi interfaccia che non sia privata. Se una funzione pubblica può essere chiamata con argomenti interi, allora può essere chiamata con qualsiasi argomento intero e il codice dovrebbe comportarsi in modo appropriato. Se una firma di funzione pubblica accetta ad es. Java Double type, quindi null, NaN, MAX_VALUE, -Inf sono tutti valori possibili. I test di unità non possono rilevare gli usi non corretti della classe perché questi test non possono testare il codice che utilizzerà questa classe, perché quel codice non è stato ancora scritto, potrebbe non essere scritto da te e sarà sicuramente al di fuori di i tuoi test di unità.

D'altra parte, questo approccio può essere valido per le proprietà private (si spera molto più numerose) - se una classe può assicurare che alcuni fatti siano sempre vere (es. la proprietà X non può mai essere nullo , la posizione intera non supera la lunghezza massima, quando viene chiamata la funzione A, tutte le strutture dei dati prerequisite sono ben formate), quindi può essere opportuno evitare di verificarlo ripetutamente per motivi di prestazioni e fare affidamento invece sui test delle unità. / p>     

risposta data 24.09.2016 - 10:19
fonte
1

La difesa contro l'uso improprio è una funzione , sviluppata a causa di un suo requisito. (Non tutte le interfacce richiedono controlli rigorosi contro l'uso improprio, ad esempio quelli interni molto ristretti.)

La funzionalità richiede test: la difesa contro l'abuso funziona davvero? L'obiettivo di testare questa funzione è provare a dimostrare che non è così: escogitare un uso non corretto del modulo che non viene catturato dai suoi controlli.

Se i controlli specifici sono una caratteristica richiesta, è davvero assurdo affermare che l'esistenza di alcuni test li rende non necessari. Se è una caratteristica di una funzione che (diciamo) genera un'eccezione quando il parametro tre è negativo, quindi non è negoziabile; lo farà.

Tuttavia, sospetto che il tuo collega abbia un senso dal punto di vista di una situazione in cui non è richiesto il controllo specifico sugli input, con risposte specifiche a input non validi: una situazione in cui c'è solo un requisito generale comprensibile per la robustezza.

I controlli sull'entrata in qualche funzione di livello superiore sono lì, in parte, per proteggere alcuni codici interni deboli o mal verificati da combinazioni impreviste di parametri (in modo tale che se il codice è ben testato, i controlli non sono necessari: il codice può solo "superare" i parametri errati).

Esiste la verità nell'idea del collega e ciò che probabilmente intende è questo: se costruiamo una funzione da pezzi di livello inferiore molto robusti, codificati in modo difensivo e testati individualmente contro ogni uso improprio, allora è possibile per il livello più alto funzione per essere robusti senza i propri controlli completi.

Se il suo contratto è violato, allora si tradurrà in un uso improprio delle funzioni di livello inferiore, magari lanciando eccezioni o altro.

L'unico problema è che le eccezioni di livello inferiore non sono specifiche per l'interfaccia di livello superiore. Se questo è un problema dipende da quali sono i requisiti. Se il requisito è semplicemente "la funzione deve essere robusta contro l'uso improprio e generare una sorta di eccezione piuttosto che crash, o continuare a calcolare con dati spazzatura", in realtà potrebbe essere coperta da tutta la robustezza dei pezzi di livello inferiore su cui è costruita.

Se la funzione ha un requisito per la segnalazione degli errori molto specifica e dettagliata relativa ai suoi parametri, allora i controlli di livello inferiore non soddisfano pienamente tali requisiti. Assicurano solo che la funzione esploda in qualche modo (non continua con una cattiva combinazione di parametri, producendo un risultato spazzatura). Se il codice client è stato scritto per catturare in modo specifico determinati errori e gestirli, potrebbe non funzionare correttamente. Il codice client potrebbe ottenere da solo, come input, i dati su cui si basano i parametri, e potrebbe aspettarsi che la funzione li controlli e per tradurre valori errati in errori specifici come documentato (in modo che possa gestirli) errori correttamente) piuttosto che alcuni altri errori che non sono gestiti e forse interrompere l'immagine del software.

TL; DR: probabilmente il tuo collega non è un idiota; stai solo parlando l'un l'altro con prospettive diverse intorno alla stessa cosa, perché i requisiti non sono completamente definiti e ognuno di voi ha un'idea diversa di quali siano i "requisiti non scritti". Pensi che quando non ci sono requisiti specifici per il controllo dei parametri, dovresti comunque codificare il controllo dettagliato; il collega pensa, lascia che il robusto codice di livello inferiore esploda quando i parametri sono sbagliati. È piuttosto improduttivo discutere dei requisiti non scritti attraverso il codice: riconoscere che non si è d'accordo sui requisiti piuttosto che sul codice. Il tuo modo di codifica riflette ciò che ritieni siano i requisiti; la maniera del collega rappresenta la sua visione dei requisiti. Se la vedi in questo modo, è chiaro che ciò che è giusto o sbagliato non è nel codice stesso; il codice è solo un proxy per la tua opinione su quale dovrebbe essere la specifica.

    
risposta data 27.09.2016 - 01:23
fonte
1

I test definiscono il contratto della tua classe.

Come corollario, l' assenza di un test definisce un contratto che include comportamento non definito . Pertanto, quando passi null a Foo::Frobnicate(Widget widget) e ne consegue un numero incalcolabile di runtime, sei ancora nel contratto della tua classe.

Più tardi decidi, "non vogliamo la possibilità di un comportamento indefinito", che è una scelta sensata. Ciò significa che devi avere un comportamento previsto per passare null a Foo::Frobnicate(Widget widget) .

E documenti quella decisione includendo un

[Test]
void Foo_FrobnicatesANullWidget_ThrowsInvalidArgument() 
{
    Given(Foo foo);
    When(foo.Frobnicate(null));
    Then(Expect_Exception(InvalidArgument));
}
    
risposta data 24.05.2018 - 17:44
fonte
1

Una buona serie di test eserciterà l'interfaccia esterna della classe e assicurerà che tali abusi generino la risposta corretta (un'eccezione o qualsiasi cosa tu definisca "corretta"). In effetti, il primo caso di test che scrivo per una classe è chiamare il suo costruttore con argomenti fuori range.

Il tipo di programmazione difensiva che tende ad essere eliminato con un approccio completamente testato unitamente è la convalida non necessaria di invarianti interni che non possono essere violati da codice esterno.

Un'utile idea che a volte impiego è quella di fornire un metodo che verifica gli invarianti dell'oggetto; il tuo metodo di demolizione può chiamarlo per verificare che le tue azioni esterne sull'oggetto non interrompano mai le invarianti.

    
risposta data 26.09.2016 - 13:12
fonte
0

I test di TDD cattureranno errori durante lo sviluppo del codice .

I limiti che ti descrivono come parte della programmazione difensiva cattureranno errori durante l'uso del codice .

Se i due domini sono gli stessi, cioè il codice che stai scrivendo viene utilizzato sempre e solo internamente da questo progetto specifico, allora è possibile che TDD precluda la necessità dei limiti di programmazione difensivi che controllano la descrizione, ma solo se quei tipi di controllo dei limiti sono specificatamente eseguiti nei test TDD .

Come esempio specifico, supponiamo che una libreria di codice finanziario sia stata sviluppata usando TDD. Uno dei test potrebbe affermare che un valore particolare non può mai essere negativo. Ciò garantisce che gli sviluppatori della libreria non abusino accidentalmente delle classi mentre implementano le funzionalità.

Ma dopo che la libreria è stata rilasciata e la sto usando nel mio programma, quei test TDD non mi impediscono di assegnare un valore negativo (supponendo che sia esposto). Il controllo dei limiti sarebbe

Il mio punto è che mentre un asserzione TDD potrebbe affrontare il problema del valore negativo se il codice è sempre usato internamente come parte dello sviluppo di un'applicazione più grande (sotto TDD), se sarà una libreria usata da altri programmatori senza framework TDD e test , limiti di verifica dei problemi.

    
risposta data 24.09.2016 - 01:21
fonte
0

TDD e programmazione difensiva vanno mano nella mano. L'utilizzo di entrambi non è ridondante, ma in realtà complementare. Quando hai una funzione, vuoi assicurarti che la funzione funzioni come descritto e scrivi test per essa; se non copri ciò che accade quando nel caso di un input non valido, di un ritorno errato, di uno stato negativo, ecc., allora non stai scrivendo i tuoi test abbastanza efficacemente e il tuo codice sarà fragile anche se tutti i tuoi test stanno passando.

Come ingegnere incorporato, mi piace utilizzare l'esempio di scrivere una funzione per aggiungere semplicemente due byte e restituire il risultato in questo modo:

uint8_t AddTwoBytes(uint8_t a, uint8_t b, uint8_t *sum); 

Ora, se hai appena fatto%% di% di conversione, funzionerebbe, ma solo con alcuni input. *(sum) = a + b e a = 1 farebbero b = 2 ; tuttavia, poiché la dimensione della somma è un byte, sum = 3 e a = 100 renderebbero b = 200 a causa di un overflow. In C, si restituirebbe l'errore in questo caso per indicare che la funzione non è riuscita; lanciare un'eccezione è la stessa cosa nel tuo codice. Non considerare il fallimento o testare come gestirli non funzionerà a lungo termine, perché se si verificano tali condizioni, non saranno gestiti e potrebbero causare un numero qualsiasi di problemi.

    
risposta data 24.09.2016 - 04:07
fonte

Leggi altre domande sui tag