2009-06-29 27 views
5

Va bene interrompere tutte le dipendenze utilizzando le interfacce solo per rendere verificabile una classe? Implica un sovraccarico significativo in fase di esecuzione a causa di molte chiamate virtuali anziché di semplici richiami di metodi.unit test in C++

In che modo lo sviluppo di test driven funziona in applicazioni C++ del mondo reale? Leggo Working Effectively With Legacy Code e mi piace molto ma non mi metto a fare pratica con il TDD.

Se eseguo un refactoring, si verifica molto spesso che devo ripetere completamente il test dell'unità a causa di enormi cambiamenti logici. Il mio codice cambia molto spesso cambia la logica fondamentale dell'elaborazione dei dati. Non vedo un modo per scrivere test unitari che non devono cambiare in un grande refactoring.

Può essere qualcuno che può indicarmi un'applicazione C++ open source che utilizza TDD per imparare dall'esempio.

risposta

5

Aggiornamento: Vedi this question too.

posso rispondere solo alcune parti qui:

va bene a rompere tutte le dipendenze che utilizzano le interfacce solo per fare una classe verificabile? Implica un sovraccarico significativo in fase di esecuzione a causa di molte chiamate virtuali anziché di semplici richiami di metodi.

Se la tua performance ne risentirebbe troppo, no (punto di riferimento!). Se il tuo sviluppo soffre troppo, no (stima sforzo extra). Se sembra che non importi molto e aiuti a lungo termine e ti aiuti con la qualità, sì.

È sempre possibile "amico" le classi di test o un oggetto TestAccessor tramite il quale i test possono indagare su di esso. Ciò evita di rendere tutto facilmente disparabile solo per i test. (sembra un bel po 'di lavoro.)

La progettazione di interfacce testabili non è facile. A volte devi aggiungere alcuni metodi extra che accedono ai componenti interni solo per il test. Ti fa rabbrividire un po 'ma è bello avere e il più delle volte quelle funzioni sono utili anche nell'applicazione reale, prima o poi.

Se eseguo un refactoring, si verifica molto spesso che devo ripetere completamente il test dell'unità a causa di enormi modifiche logiche. Il mio codice cambia molto spesso cambia la logica fondamentale dell'elaborazione dei dati. Non vedo un modo per scrivere test unitari che non devono cambiare in un grande refactoring.

I grandi rifattori per definizione cambiano molto, compresi i test. Sii felice di averli come testeranno la roba anche dopo il refactoring.

Se si impiegano più tempo per il refactoring di nuove funzionalità, forse dovresti considerare di pensare un po 'di più prima della codifica per trovare interfacce migliori in grado di resistere a più cambiamenti. Inoltre, scrivere dei test unitari prima che le interfacce siano stabili è un dolore, indipendentemente da quello che fai.

Più codice hai contro un'interfaccia che cambia molto, più codice dovrai cambiare ogni volta. Penso che il tuo problema stia lì. Sono riuscito a disporre di interfacce sufficientemente stabili nella maggior parte dei casi e a rifattare le parti solo di tanto in tanto.

Spero che aiuti.

+0

Non è che non penso prima della codifica. Molto spesso i requisiti cambiano dopo che il cliente ha visto l'implementazione iniziale di una nuova funzionalità. Un altro motivo è che a volte è necessario eseguire un'implementazione rapida e sporca per motivi di marketing. In questo caso non mi preoccupo dei test unitari, ma a volte l'hack veloce e sporco diventa la cosa reale a causa di un calendario serrato. Non è facile rendere questo pasticcio più tardi. – frast

+0

Scusa, non intendevo implicare esattamente quello. ;) So che a volte la vita reale è difficile da lavorare. Quick'n dirty ha un debito, un test mancante, un necessario refactoring/cleanup o di solito entrambi. Si paga prima in contanti (lavoro appropriato) o si paga in affitto dopo (pulizia). Non andare in giro. È difficile lavorare in condizioni in cui non puoi influenzarlo. (Ci sono stato ...) – Macke

3

Ho usato macro di routine, #if e altri trucchi per il preprocessore per le dipendenze di "mock-out" ai fini del test di unità in C e C++, esattamente perché con tali macro non devo pagare nessun tempo di esecuzione costo quando il codice viene compilato per la produzione anziché per il test. Non elegante, ma ragionevolmente efficace.

Per quanto riguarda i refactoring, potrebbe richiedere di cambiare i test quando sono così largamente grandi e intrusivi come descrivi. Però non mi ritrovo a rifare così drasticamente tutto questo spesso.

+1

Penso a una soluzione, ma temo che possa accadere accidentalmente che sto testando non il "codice reale" a causa della definizione del preprocessore. Ma penso che valga la pena provarlo. – frast

+0

Sì, le macro e gli altri hack del preprocessore sono cose fragili se non usate con cura e secondo schemi specifici e limitati - questa è la parte "non elegante" nella mia risposta.Il macro-equivalente dell'iniezione di dipendenza (a volte anche ottenibile da template o typedef, come dice @jaif, in casi semplici) è un caso piuttosto limitato e disciplinato, e questo è tutto ciò che serve per "deridere" le tue dipendenze per scopi di test unitario. –

1

La risposta ovvia sarebbe quella di calcolare le dipendenze utilizzando modelli anziché interfacce. Ovviamente questo potrebbe danneggiare i tempi di compilazione (dipende esattamente da come lo si implementa), ma dovrebbe eliminare almeno il sovraccarico di runtime. Una soluzione leggermente più semplice potrebbe essere quella di basarsi su un insieme di typedef, che possono essere sostituiti con alcune macro o simili.

1

Per quanto riguarda la tua prima domanda, raramente vale la pena di rompere le cose solo per motivi di testing, anche se a volte potresti dover rompere le cose prima di renderle migliori come parte del tuo refactoring. Il criterio più importante per un prodotto software è che funzioni, non che sia testabile. Testabile è importante solo perché ti aiuta a realizzare un prodotto che è più stabile e funziona meglio per i tuoi utenti finali.

Una grande parte dello sviluppo basato su test è la selezione di piccole parti atomiche del codice che non è probabile che cambino per il test dell'unità. Se devi riscrivere un sacco di test unitari a causa di enormi modifiche logiche, potresti dover testare a un livello più fine, o riprogettare il tuo codice in modo che sia più stabile. Un design stabile non dovrebbe cambiare drasticamente nel tempo e il test non ti aiuterà a evitare un enorme refactoring che diventa necessario. Tuttavia, se eseguito correttamente, i test possono fare in modo che quando si refactoring le cose si possa essere più sicuri che il refactoring abbia avuto successo, supponendo che ci siano alcuni test che non devono essere modificati.

1

Va bene interrompere tutte le dipendenze utilizzando le interfacce solo per rendere verificabile una classe? Implica un sovraccarico significativo in fase di esecuzione a causa di molte chiamate virtuali anziché di semplici richiami di metodi.

Penso che sia corretto interrompere le dipendenze, poiché ciò porterà a interfacce migliori.

Se eseguo un refactoring, si verifica molto spesso che devo ripetere completamente il test dell'unità a causa di enormi modifiche logiche. Il mio codice cambia molto spesso cambia la logica fondamentale dell'elaborazione dei dati. Non vedo un modo per scrivere test unitari che non devono cambiare in un grande refactoring.

Non si otterrà un giro di questi grandi refettori in qualsiasi lingua poiché i test dovrebbero esprimere il vero intento del codice. Quindi se la logica cambia, i test devono cambiare.

Forse non stanno realmente facendo TDD, come:

  1. Creare un test che fallisce
  2. creare il codice per superare la prova
  3. Creare un altro test che fallisce
  4. Fissare il codice per superare entrambi i test
  5. Risciacquare e ripetere finché non si ritiene di avere sufficienti test che mostrano cosa dovrebbe fare il codice

Questi passaggi indicano che dovresti apportare modifiche minori e non di grandi dimensioni. Se stai con quest'ultimo, non puoi sfuggire ai grandi refactories.Nessuna lingua ti salverà da questo, e il C++ sarà il peggiore di loro a causa dei tempi di compilazione, dei link time, dei cattivi messaggi di errore, ecc. Ecc.

In realtà sto lavorando a un software del mondo reale scritto in C++ con un codice legacy ENORME sotto di esso. Usiamo TDD e stiamo davvero aiutando ad evolvere il design del software.

0

Se faccio un refactoring si verifica molto spesso che devo completamente unit test riscrittura a causa dei cambiamenti di logica massicce. ... Non vedo il modo di scrivere test unitari che non devono cambiare in un grande refactoring.

There are multiple layers of testing e alcuni di questi livelli non si interromperanno anche dopo grandi cambiamenti logici. Il test unitario, d'altra parte, ha lo scopo di testare l'interno di metodi e oggetti, e dovrà cambiare più spesso di quello. Non c'è niente di sbagliato, necessariamente. È solo come stanno le cose.

È [...] corretto interrompere tutte le dipendenze utilizzando le interfacce solo per rendere verificabile una classe?

It's definitely OK to design classes to be more testable. Questo è parte dello scopo di TDD, dopo tutto.

Esso comporta un sovraccarico significativo in fase di esecuzione a causa di molte chiamate virtuali anziché di semplici richiami di metodi.

Quasi ogni azienda ha qualche lista di regole che tutti i dipendenti sono chiamati a seguire. Le aziende clueless elencano semplicemente ogni buona qualità a cui possono pensare ("i nostri dipendenti sono efficienti, responsabili, etici e non tagliano mai le punte"). Le aziende più intelligenti classificano effettivamente le loro priorità. Se qualcuno presenta un modo non etico di essere efficiente, l'azienda lo fa? Le migliori aziende non solo stampano opuscoli che dicono come vengono classificate le priorità, ma assicurano anche che la direzione segua la classifica.

È del tutto possibile che un programma sia efficiente e facilmente verificabile. Tuttavia ci sono momenti in cui è necessario scegliere quale è più importante. Questa è una di quelle volte. Non so quanto sia importante l'efficienza per te e il tuo programma, ma tu sì. Quindi "preferiresti avere un programma lento, ben testato, o un programma veloce senza copertura totale del test?"

+0

Hai fatto ottimi punti. Alla tua ultima domanda penso che dovremmo pensare alla copertura delle prestazioni e dei test da un caso all'altro. Non tutte le parti della nostra applicazione richiedono prestazioni massime. – frast

0

Esso comporta notevole sovraccarico a runtime causa di molti chiamate virtuali invece di chiamate ai metodi semplici.

Ricordare che è un overhead di chiamata virtuale solo se si accede al metodo tramite un puntatore (o un riferimento) all'interfaccia o all'oggetto. Se si accede al metodo attraverso un oggetto concreto nello stack, questo non avrà il sovraccarico virtuale e può anche essere in linea.

Inoltre, non assumere mai che questo sovraccarico sia grande prima di profilare il codice. Quasi sempre, una chiamata virtuale è senza valore se si confronta a ciò che sta facendo il tuo metodo. (La maggior parte della penalità deriva dal non essere in grado di allineare un metodo a una riga, non dall'indirizzamento extra della chiamata).