Breve introduzione a questa domanda. Ho usato TDD e ultimamente BDD per oltre un anno. Uso tecniche come il deridere per rendere più efficiente la scrittura dei miei test. Ultimamente ho avviato un progetto personale per scrivere un piccolo programma di gestione del denaro per me stesso. Dato che non avevo un codice legacy, era il progetto perfetto per iniziare con TDD. Purtroppo non ho provato così tanto la gioia di TDD. Mi ha persino rovinato così tanto il divertimento che ho rinunciato al progetto.
Qual'era il problema? Bene, ho usato l'approccio simile a TDD per consentire ai test / requisiti di evolvere la progettazione del programma. Il problema era che oltre la metà del tempo di sviluppo per quanto riguarda i test di scrittura / refactoring. Quindi alla fine non volevo implementare altre funzionalità perché avrei dovuto refactoring e scrivere a molti test.
Al lavoro ho un sacco di codice legacy. Qui scrivo sempre più test di integrazione e accettazione e meno test unitari. Questo non sembra essere un cattivo approccio poiché i bug sono per lo più rilevati dai test di accettazione e integrazione.
La mia idea era che alla fine avrei potuto scrivere più test di integrazione e accettazione rispetto ai test unitari. Come ho detto per il rilevamento di bug, i test unitari non sono migliori dei test di integrazione / accettazione. Anche i test unitari sono validi per il design. Da quando ne scrivevo molti, le mie lezioni sono sempre progettate per essere testabili. Inoltre, l'approccio che consente ai test / requisiti di guidare la progettazione conduce, nella maggior parte dei casi, a una progettazione migliore. L'ultimo vantaggio dei test unitari è che sono più veloci. Ho scritto abbastanza test di integrazione per sapere che possono essere quasi veloci quanto i test unitari.
Dopo aver guardato attraverso il web ho scoperto che ci sono idee molto simili alle mie menzionate qua e là . Cosa ne pensi di quest idea?
modificare
Rispondendo alle domande un esempio in cui il design era buono, ma avevo bisogno di un enorme refactoring per il prossimo requisito:
Inizialmente c'erano alcuni requisiti per eseguire determinati comandi. Ho scritto un parser di comandi estensibile - che analizzava i comandi da una sorta di prompt dei comandi e chiamava quello corretto sul modello. Il risultato è stato rappresentato in una classe del modello di vista:
Non c'era niente di sbagliato qui. Tutte le classi erano indipendenti l'una dall'altra e potevo facilmente aggiungere nuovi comandi, mostrare nuovi dati.
Il requisito successivo era che ogni comando dovesse avere una propria rappresentazione della vista - una sorta di anteprima del risultato del comando. Ho riprogettato il programma per ottenere un design migliore per il nuovo requisito:
Anche questo era buono perché ora ogni comando ha il suo modello di vista e quindi la sua anteprima.
Il fatto è che il parser dei comandi è stato modificato per utilizzare un'analisi dei comandi basata su token ed è stato rimosso dalla sua capacità di eseguire i comandi. Ogni comando ha il proprio modello di visualizzazione e il modello di visualizzazione dati conosce solo il modello di visualizzazione comandi corrente che non conosce i dati che devono essere mostrati.
Tutto quello che volevo sapere a questo punto è, se il nuovo design non ha violato alcun requisito esistente. Non ho dovuto cambiare QUALUNQUE del mio test di accettazione. Ho dovuto refactoring o eliminare quasi OGNI test unitario, che era un enorme mucchio di lavoro.
Quello che volevo mostrare qui è una situazione comune che si è verificata spesso durante lo sviluppo. Non ci sono stati problemi con il vecchio o il nuovo design, sono semplicemente cambiati naturalmente con i requisiti: come l'ho capito, questo è uno dei vantaggi di TDD, che il design si evolve.
Conclusione
Grazie per tutte le risposte e discussioni. In sintesi di questa discussione ho pensato a un approccio che testerò con il mio prossimo progetto.
- Prima di tutto scrivo tutti i test prima di implementare qualcosa come ho sempre fatto.
- Per esigenze scrivo inizialmente alcuni test di collaudo che testano l'intero programma. Quindi scrivo alcuni test di integrazione per i componenti in cui devo implementare il requisito. Se esiste un componente che lavora a stretto contatto con un altro componente per implementare questo requisito, scriverei anche alcuni test di integrazione in cui entrambi i componenti vengono testati insieme. Ultimo ma non meno importante se devo scrivere un algoritmo o qualsiasi altra classe con un'alta permutazione - ad esempio un serializzatore - scriverei test unitari per queste classi particolari. Tutte le altre classi non sono testate ma eventuali test unitari.
- Per i bug il processo può essere semplificato. Normalmente un bug è causato da uno o due componenti. In questo caso, scriverei un test di integrazione per i componenti che verifica il bug. Se fosse correlato a un algoritmo, scriverei solo un test unitario. Se non è facile rilevare il componente in cui si verifica il bug, scriverei un test di accettazione per individuare il bug - questa dovrebbe essere un'eccezione.