Implementazioni economiche nel TDD fondamentale

-1

Questa è una domanda riguardante l'approccio fondamentale del TDD, quindi l'esempio qui sotto è il più semplice possibile che potrebbe far sembrare un po 'inutile; ma naturalmente la domanda si applica anche a situazioni più complicate.

Alcuni colleghi e io stiamo attualmente discutendo e provando alcuni metodi di codifica di base del TDD. Ci siamo imbattuti in domande su come gestire soluzioni a basso costo per i TC esistenti ma non esaustivi. In TDD si scrive un TC che fallisce, quindi implementa tutto ciò che serve (e non di più!) Per consentire al TC di passare. Quindi il compito da svolgere sarebbe quello di rendere il TC verde con il minor sforzo possibile. Se questo significa implementare una soluzione che utilizza la conoscenza interna del TC, così sia. Il ragionamento era che i TC successivi avrebbero comunque verificato la correttezza più generale, quindi la prima soluzione avrebbe dovuto essere migliorata allora (e solo allora).

Esempio:

Vogliamo scrivere una funzione di confronto per una struttura dati con tre campi. Il nostro confronto restituirà se i valori dati sono uguali in tutti e tre i campi (o differiscono in almeno uno). Il nostro primo TC scritto controlla solo se una differenza nei primi due valori viene rilevata correttamente: passa (a,b,c) e (a,b,c) e verifica il corretto rilevamento dell'uguaglianza, quindi passa (a,b,c) e (x,b,c) e verifica una corretto rilevamento della disuguaglianza.

Ora l'approccio economico dovrebbe anche implementare solo un confronto del primo campo, perché questo dovrebbe essere sufficiente per passare questo TC. Tieni presente che ciò può essere fatto perché sappiamo che i test successivi controlleranno anche l'uguaglianza degli altri due campi.

Ma ovviamente non sembra molto utile implementare solo una soluzione (più o meno) senza senso; ogni programmatore che lo fa lo farebbe con la consapevolezza di scrivere un bug. Ovviamente sembra più naturale scrivere subito un confronto decente nella prima iterazione.

D'altro canto, scrivere una soluzione corretta senza avere un TC che la controlli potrebbe portare alla situazione in cui un tale TC che verifica il comportamento più a fondo non verrà mai scritto. Quindi c'è un comportamento che è stato scritto senza avere un TC per questo (cioè, che non è stato sviluppato test-driven).

Forse un approccio corretto è quello di non scrivere TC così rudimentali (come quello che controlla solo il primo campo) in primo luogo, ma ciò significherebbe richiedere TC perfetti in prima iterazione (e, naturalmente, in situazioni complesse probabilmente uno non sempre scrivere TC perfetti).

Quindi come si dovrebbe trattare con TC rudimentali? Implementare una soluzione economica o no?

    
posta Alfe 09.02.2015 - 16:39
fonte

6 risposte

4

Penso che il tuo problema si presenti solo perché il requisito è molto semplice e la soluzione che confronta tutti e 3 i valori in una sola volta, forse è solo un one-liner. Avere requisiti leggermente più complessi, e avrà perfettamente senso non implementare nulla oltre lo scopo dei casi di test già implementati.

Tuttavia, il tuo "approccio economico" ha in realtà un vantaggio che lo rende meno insensato di quanto si possa pensare: è molto meglio non dimenticare di aggiungere tutti i casi di test importanti. Se si implementa il confronto a tre valori contemporaneamente, esiste una certa probabilità che si possano omettere ulteriori casi di test, poiché si è già nello stato mentale di "essere stati fatti". Se, tuttavia, sai che il tuo codice non è ancora pronto, e ti costringi a non cambiarlo senza ulteriori casi di test, è molto più alto che prenderà il tempo e aggiungerà quei casi di test .

Soprattutto per scopi di apprendimento TDD, consiglio " TDD come se lo hai inteso ", un esercizio inventato da Keith Braithwaite per addestrare gli sviluppatori a fare TDD in passi ancora più piccoli. Applicato al tuo esempio: in questo esercizio, il tuo primo passo non sarebbe nemmeno quello di implementare una funzione con un controllo di uguaglianza, prima dovresti implementare il controllo di uguaglianza nel codice di test e poi rifattarlo in seguito alla funzione di confronto.

    
risposta data 09.02.2015 - 16:56
fonte
2

L'implementazione della soluzione "economica" è una buona idea, non solo perché ti costringe a scrivere test che coprono tutti i comportamenti previsti, ma anche perché a volte finisce per scrivere una soluzione più semplice di quella che potresti avere nella tua testa .

Un buon esempio di questo è dato nel libro Beautiful Code, che descrive il framework di test FIT. Questo sistema funziona analizzando i documenti HTML per le tabelle contenenti dati da collegare ai casi di test e quindi produce un documento di output con la riga della tabella colorata in rosso o verde. Un approccio ingenuo sarebbe utilizzare un parser HTML, ma semplicemente prendendo i test uno alla volta e scrivendo la soluzione più semplice possibile ad ogni passaggio, gli autori sono arrivati a una soluzione molto più semplice che utilizza solo una semplice manipolazione delle stringhe.

Un'altra cosa da considerare è "Transformation Priority Premise" di Robert C Martin. Questa è un'estensione di TDD che fornisce alcune regole aggiuntive per scrivere il codice come una sequenza di semplici trasformazioni (simile al refactoring, ma con l'obiettivo di modificare il comportamento in modi controllati piuttosto che conservarlo), e può dare risultati molto interessanti. Vale la pena indagare ulteriormente, credo.

    
risposta data 09.02.2015 - 17:03
fonte
2

Vorrei dire che la fase di refactoring nel ciclo TDD è il secondo passo più importante e quello che fa il passaggio da codice più semplice ed economico a codice previsto. L'esempio più semplice che riesco a pensare è un costruttore.

Il mio primo test può essere che la mia nuova classe riceve un determinato parametro e controlla il valore di un parametro. È abbastanza facile da implementare, giusto? Posso farlo passare molto a buon mercato. Ma una volta che passa l'implementazione viene lasciata a un refactoring.

  • Posso implementare un getter / setter in quel parametro.
  • O forse la mia classe fa una richiesta get ad un'API esterna (derisa) e memorizza il risultato nel parametro.
  • O quel parametro è il risultato dell'operazione mat.

Ma tutti questi casi sono testati con la premessa che tu inserisca un valore e che si aspetti un altro, che il funzionamento posteriore sia irrilevante, che tu voglia lo stesso risultato. Questo è per me il grande vantaggio delle soluzioni economiche.

    
risposta data 12.02.2015 - 00:28
fonte
1

Molto di questo si riduce a quello che è "economico, inutile e ... il minimo sforzo possibile".

Ecco un altro semplice esempio: scrivi una funzione che restituisca il risultato di due numeri sommati insieme. Test:

check func(2, 2) = 4
// Simple:
  func(a, b) { return a + b}

Mi piacerebbe pensare che questa sia la prima strategia che prenderei. Sembra abbastanza semplice per me, ma ora che ci penso, per quanto riguarda un approccio ancora più semplice:

//Simpler
func(a,b) { return 4}

È più semplice e probabilmente inutile, ma ha richiesto davvero meno sforzi? Sembra che la prima soluzione ragionevole che scoppia nella tua testa richiederebbe il minimo sforzo a meno che la quantità di digitazione non sia un problema di produttività. Se qualcosa è così ovvio che è una perdita di tempo, non farlo. Questo è il vantaggio di avere esperienza e una possibile spiegazione del perché alcune persone si sentano brave programmatori possono essere 10 volte più produttive perché non perdono tempo a ripetere errori o scrivere codice di cui non hanno bisogno.

    
risposta data 09.02.2015 - 19:40
fonte
0

Hai ragione nel ritenere che i Test Case perfetti e perfetti non siano realistici per la maggior parte del codice (con una possibile eccezione per il codice Mars Rover).

Direi che è necessario onorare l'intento del caso / specifica del test oltre alla "lettera della legge". Nel tuo esempio lo sviluppatore ha l'opportunità di scrivere un bug ovvio. L'azione corretta sarebbe quella di risolvere prima il caso di test, quindi scrivere il codice.

I test sono anche codice, e sono anche soggetti a bug, implementazioni incomplete, ecc.

    
risposta data 09.02.2015 - 16:56
fonte
0

Il problema che hai è davvero nel pensare di scrivere i tuoi casi di test. Ti stai bloccando nel TDD Dogma di aver bisogno di un solo test fallito alla volta.

Si definisce il comportamento che si desidera che una funzione determini se due gruppi di tre elementi abbiano gli stessi valori. Quindi si definisce il test case controllando solo il primo. Sapete dalla definizione del problema che avete almeno quattro casi da testare (uguali, 1 ° non uguale, 2 ° non uguale, 3 ° non uguale). Quindi devi creare immediatamente questi casi di test in modo da definire il comportamento del codice.

Nel tuo esempio, il comportamento è che nei due gruppi il controllo del primo elemento è uguale. Questa è una definizione incompleta del comportamento. E finiresti per creare la soluzione ingenua che hai e quindi dovrai fare più lavoro mentre prosegui.

Usa i tuoi test per definire un comportamento e scrivere test che falliscono e passare solo quando quel comportamento è stato creato correttamente.

    
risposta data 09.02.2015 - 20:18
fonte

Leggi altre domande sui tag