2009-08-25 18 views
5

Alcune settimane fa ho iniziato il mio primo progetto con TDD. Fino ad ora, ho letto solo un libro al riguardo.Come sviluppare metodi complessi con TDD

La mia preoccupazione principale: come scrivere test per metodi/classi complessi. Ho scritto una classe che calcola una distribuzione binomiale. Quindi, un metodo di questa classe prende n, k e p come input e calcola il resp. probabilità. (In effetti fa un po 'di più, è per questo che ho dovuto scriverlo da solo, ma atteniamo a questa descrizione della classe, per semplicità dell'argomento.)

Quello che ho fatto per testare questo metodo è: copiare alcuni tavoli con diverse n ho trovato nel web nel mio codice, selezionando a caso una voce in questa tabella, alimentato il resp. valori per n, k e p nella mia funzione e ho osservato se il risultato era vicino al valore nella tabella. Lo ripeto un numero di volte per ogni tavolo.

Questo funziona tutto bene ora, ma dopo aver scritto il test, ho dovuto tankare per alcune ore per codificare realmente la funzionalità. Dalla lettura del libro, ho avuto l'impressione di non dover programmare più a lungo di alcuni minuti, finché il test non è di nuovo verde. Cosa ho fatto di sbagliato qui? Naturalmente ho rotto questo compito in molti modi, ma sono tutti privati.

Una domanda correlata: era una cattiva idea scegliere numeri casuali dal tavolo? In caso di errore, visualizzerò il seme casuale utilizzato da questa esecuzione, in modo che possa riprodurre il bug.

+0

"Ho avuto l'impressione di non dover programmare più di qualche minuto, finché il test non è di nuovo verde." Dove - in particolare - hai avuto questa impressione? Si prega di fornire la citazione o riferimento. Questo è raramente vero, e sono curioso di sapere dove hai letto/visto/sentito. –

+0

Era in un libro tedesco, "Testegetriebene Entwicklung mit JUnit & FIT", di Frank Westphal, prima edizione. Per esempio. a pagina 13, le prime due frasi. – matthias

+1

E visto che molto probabilmente non hai accesso al libro, provo una traduzione: "L'interplay tra lo sviluppo basato su test e la progettazione semplice, genera un ciclo di codifica minuzioso, infatti non si codifica più a lungo di solo pochi minuti, senza chiudere il ciclo di feedback per mezzo di test. " (Bene, mi sto avvicinando ai limiti del mio inglese qui, spero che questa traduzione sia corretta.) – matthias

risposta

3

"Ho avuto l'impressione di non dover programmare più a lungo di alcuni minuti, finché il test non è di nuovo verde. Che cosa ho fatto di sbagliato qui?"

Westphal è corretto fino a un punto.

Alcune funzionalità iniziano in modo semplice e possono essere semplicemente verificate e codificate semplicemente.

Alcune funzionalità non vengono avviate in modo semplice. Semplice è difficile da raggiungere. EWD dice che la semplicità non è valutata perché è così difficile da raggiungere.

Se il corpo della funzione è difficile da scrivere, non è semplice. Ciò significa che devi lavorare molto più duramente per ridurlo a qualcosa di semplice.

Dopo aver finalmente raggiunto la semplicità, anche tu puoi scrivere un libro che mostri quanto sia semplice.

Fino a raggiungere la semplicità, ci vorrà molto tempo per scrivere le cose.

"È stata una cattiva idea scegliere numeri casuali dal tavolo?"

Sì. Se si dispone di dati di esempio, eseguire il test contro tutti i dati di esempio. Usa un loop o qualcosa del genere e prova tutto ciò che puoi eventualmente testare.

Non selezionare una riga - in modo casuale o meno, selezionare tutte le righe.

+0

Grazie per il tuo commento. Ho la sensazione che tu abbia ragione. Devo semplicemente imparare molto. E finché non avrò mesi/anni di esperienza, devo accettare che le mie soluzioni non sono ottimali. – matthias

+0

Non è una questione di "ottimale". Il problema è che "semplice" è davvero, davvero difficile da raggiungere. I pochi preziosi possono fare un buon lavoro per ottenere la vera semplicità. Gli esempi di libri controversi sono i peggiori, dal momento che l'autore ha avuto molto tempo per arrivare alla semplicità. Tutti dobbiamo lavorarci; possono nascondere la grande quantità di sforzo richiesto per ottenere un semplice esempio. –

0

È difficile rispondere alla domanda senza conoscere un po 'di più le cose che si desidera implementare. Sembra che non fossero facilmente parziali nelle parti testabili. O la funzionalità funziona nel suo complesso o no. Se questo è il caso, non c'è da meravigliarsi delle ore di strumento per implementarlo.

Per quanto riguarda la seconda domanda: Sì, penso che sia una cattiva idea rendere il dispositivo di prova casuale. Perché l'hai fatto in primo luogo? La modifica del dispositivo cambia il test.

1

Si dovrebbe usare TDD per piccoli passi. Prova a pensare a test che richiedono meno codice da scrivere. Quindi scrivi il codice. Quindi scrivi un altro test e così via.

Provare a suddividere il problema in problemi più piccoli (probabilmente hai utilizzato altri metodi per completare il codice). Potresti TDD questi metodi più piccoli.

--edit - sulla base dei commenti

metodi di prova privati ​​non è necessariamente una brutta roba. A volte contengono davvero dettagli di implementazione, ma a volte potrebbero anche comportarsi come un'interfaccia (in questo caso, potresti seguire il mio suggerimento nel prossimo paragrafo).

Un altra opzione è quella di creare altre classi (realizzato con le interfacce che vengono iniettati) a prendere alcune delle responsabilità (forse alcuni di questi metodi più piccoli), e verificare separatamente, e beffe quando prova la tua classe principale.

Infine, non vedo passare più tempo a programmare come un problema veramente grande. Alcuni problemi sono molto più complessi da implementare che testare e richiedono molto tempo per pensare.

+0

Sì, potrei TDD questi metodi più piccoli, ma poi ho avuto per testare il metodo privato S. E come ho capito, fanno parte dei dettagli di implementazione e non dovrebbero essere testati. – matthias

+0

ha appena modificato la mia risposta –

+0

Se aiuta a fare TDD nel modo desiderato, è possibile iniziare con questi metodi privati ​​che sono pubblici/protetti e testare quelle parti più piccole di funzionalità. Una volta creati i metodi pubblici, che li chiamano, è possibile rimuovere i test che testano direttamente i metodi privati. Dovreste anche chiedervi se averli a visibilità protetta/predefinita, al fine di testare lo stesso pacchetto, sta per rovinare l'incapsulamento. In caso contrario, direi che è un buon compromesso. – Grundlefleck

0

Evitare di sviluppare metodi complessi con TDD fino a quando non si sono sviluppati metodi semplici come elementi costitutivi per i metodi più complessi. In genere il TDD viene utilizzato per creare una quantità di funzionalità semplice che potrebbe essere combinata per produrre un comportamento più complesso. I metodi/le classi complessi dovrebbero essere sempre in grado di essere suddivisi in parti più semplici, ma non è sempre ovvio come e sia spesso specifico del problema. Il test che hai scritto suona come potrebbe essere più di un test di integrazione per assicurarsi che tutti i componenti funzionino correttamente, anche se la complessità del problema che descrivi confina solo con il limite di richiedere un set di componenti per risolverlo. La situazione che lei descrive suona così:

class A { doLotsOfStuff pubblico() // chiamata doTask1..n doTask1 privato() doTask2 privato() doTask3 privato()}

Avrete trovarlo abbastanza difficile da sviluppare con TDD se inizi a scrivere un test per la più grande unità di funzionalità (es. doLotsOfStuff()). Rompendo il problema in blocchi più maneggevoli e avvicinandoci alla fine della più semplice funzionalità, sarai anche in grado di creare test più discreti (molto più utili dei test che controllano tutto!). Forse il vostro potenziale soluzione potrebbe essere riformulata così:

class A { doLotsOfStuff pubblico() // chiama doTask1..n doTask1 pubblico() pubblico doTask2() pubblico doTask3()}

Anche se i tuoi metodi privati ​​possono essere dettagli dell'implementazione, non è un motivo per evitare di testarli separatamente. Proprio come molti problemi, un approccio di divisione e conquista si dimostrerebbe efficace qui. La vera domanda è quale dimensione è un pezzo di funzionalità adeguatamente testabile e gestibile? Solo tu puoi rispondere in base alla tua conoscenza del problema e al tuo giudizio sull'applicazione delle tue capacità al compito.

1

Si è corretti sui refact rapidi e brevi, raramente si passa più di qualche minuto tra la ricostruzione/test, non importa quanto sia complicato il cambiamento. Ci vuole un po 'di pratica.

Il test che hai descritto è più un test di sistema che un test di unità. Un test unitario non tenta mai di testare più di un singolo metodo - al fine di ridurre la complessità, probabilmente si dovrebbe risolvere il problema in molti modi.

Il test di sistema dovrebbe probabilmente essere eseguito dopo aver sviluppato la funzionalità con test di piccole unità su piccoli metodi diretti.

Anche se i metodi stanno solo prendendo una parte della formula da un metodo più lungo, si ottiene il vantaggio della leggibilità (il nome del metodo dovrebbe essere più leggibile rispetto alla parte della formula che sostituisce) e se i metodi sono definitivi il Le JIT dovrebbero indicarle in modo da non perdere velocità.

D'altra parte, se la tua formula non è così grande, forse basta scrivere tutto in un solo metodo e testarlo come hai fatto tu e prendere il tempo di inattività - le regole sono fatte per essere infranti.

6

Non sono d'accordo con le persone che dicono che è ok testare il codice privato, anche se li si fa in classi separate. Dovresti testare i punti di accesso alla tua applicazione (o al tuo grimorio, se si tratta di una libreria che stai codificando). Quando testate il codice privato, limitate le vostre possibilità di ri-factoring per dopo (poiché il refactoring delle vostre classi private significa refactoring del vostro codice di test, che dovreste astenervi dal fare). Se finisci di riutilizzare altrove questo codice privato, allora certo, crea classi separate e provale, ma fino a quando non lo fai, supponi che non ne hai bisogno.

Per rispondere alla tua domanda, penso che sì, in alcuni casi, non è una situazione da "2 minuti fino a quando non vai verde". In questi casi, penso che sia ok per i test impiegare molto tempo per diventare ecologici. Ma la maggior parte delle situazioni sono "2 minuti finché non si diventa verde". Nel tuo caso (non conosco lo squat sulla distribuzione binomiale), hai scritto che hai 3 argomenti, n, k e p. Se mantieni costante k e p, la tua funzione è più semplice da implementare? Se sì, dovresti iniziare creando test che abbiano sempre costante k e p. Al termine del test, introdurre un nuovo valore per k, quindi per p.

0

Penso che lo stile di test che si possiede sia assolutamente appropriato per il codice che è principalmente un calcolo. Anziché selezionare una riga casuale dalla tabella dei risultati noti, sarebbe preferibile limitarsi a codificare i casi limite significativi. In questo modo i tuoi test verificano costantemente la stessa cosa, e quando uno si rompe sai cosa fosse.

Sì TDD prescrive brevi intervalli dal test all'implementazione, ma quello che hai giù è ancora ben oltre gli standard che troverai nel settore. Ora puoi fare affidamento sul codice per calcolare come dovrebbe, e puoi refactoring/estendere il codice con un certo grado di certezza che non lo stai infrangendo.

Man mano che si apprendono più tecniche di test, è possibile trovare un approccio diverso che accorcia il ciclo rosso/verde. Nel frattempo, non sentirti a disagio. È un mezzo per un fine, non un fine in sé.