I test unitari portano a generalizzazioni premature (in particolare nel contesto del C ++)?

20

Note preliminari

Non entrerò nella distinzione tra i diversi tipi di test, ci sono già alcuni domande su questi siti in merito.

Prenderò ciò che è lì e ciò dice: test delle unità nel senso di "testare la più piccola unità isolabile di un'applicazione" da cui deriva effettivamente questa domanda

Il problema di isolamento

Che cos'è la più piccola unità isolabile di un programma. Bene, a quanto vedo io, molto (altamente?) Dipende da quale linguaggio stai codificando.

Micheal Feathers parla del concetto di una cucitura : [WEwLC, p31 ]

A seam is a place where you can alter behavior in your program without editing in that place.

E senza entrare nei dettagli, capisco che una cucitura - nel contesto dei test unitari - sia un posto in un programma in cui il tuo "test" può interfacciarsi con la tua "unità".

Esempi

Il test unitario, specialmente in C ++, richiede dal codice sotto test di aggiungere più giunture che sarebbero strettamente richieste per un dato problema.

Esempio:

  • Aggiunta di un'interfaccia virtuale in cui l'implementazione non virtuale sarebbe stata sufficiente
  • Splitting - generalizing (?) - una (piccola) classe ulteriormente "just" per facilitare l'aggiunta di un test.
  • Suddividere un progetto a esecuzione singola in librerie apparentemente "indipendenti", "solo" per facilitare la compilazione indipendente per i test.

La domanda

Proverò alcune versioni che si spera possano chiedere lo stesso punto:

  • È il modo in cui i Test di unità richiedono uno per strutturare il codice di un'applicazione "solo" vantaggioso per i test di unità o è effettivamente vantaggioso per la struttura delle applicazioni.
  • La generalizzazione del codice è necessaria per renderlo unit-testable utile per qualsiasi ma test dell'unità?
  • L'aggiunta di test di unità obbliga uno a generalizzare inutilmente?
  • L'unità di forma prova a forzare sul codice "sempre" anche una buona forma per il codice in generale visto dal dominio problematico?

Ricordo una regola empirica che diceva non generalizzare fino a quando non è necessario / fino a quando non c'è un secondo posto che utilizza il codice. Con Test unitari, c'è sempre un secondo posto che utilizza il codice, ovvero il test unitario. Quindi questa ragione è sufficiente per generalizzare?

    
posta Martin Ba 14.11.2011 - 14:41
fonte

5 risposte

23

Unit test -- especially in C++ -- require from the code under test to add more seams that would be strictly called for for a given problem.

Solo se non consideri di testare una parte integrale del problem solving. Per qualsiasi problema non banale, dovrebbe essere, non solo nel mondo del software.

Nel mondo dell'hardware, questo è stato appreso molto tempo fa - nel modo più duro. I produttori di varie apparecchiature hanno imparato attraverso i secoli innumerevoli ponti che cadono, macchine che esplodono, CPU che fumano, ecc. Ecc. Cosa stiamo imparando ora nel mondo del software. Tutti loro costruiscono "cuciture extra" nei loro prodotti per renderli testabili. La maggior parte delle nuove auto al giorno d'oggi dispone di porte diagnostiche per i riparatori per ottenere dati su ciò che accade all'interno del motore. Una parte significativa dei transistor su ogni CPU serve a scopi diagnostici. Nel mondo dell'hardware, ogni parte dei costi "extra" della roba e quando un prodotto è prodotto da milioni, questi costi sicuramente sommano a ingenti somme di denaro. Tuttavia, i produttori sono disposti a spendere tutti questi soldi per la testabilità. Probabilmente hanno capito (o imparato nel modo più duro) che il rischio che il loro prodotto fallisca nelle mani (o sotto i barboni) degli utenti finali e la conseguente perdita di reputazione, cause legali, cattive pubbliche relazioni ecc. Ecc. Ecc. superiore al costo di progettare e rendere testabile il prodotto fin dall'inizio.

Tornando al mondo del software, il C ++ è in effetti più difficile da testare unitamente ai linguaggi successivi con caricamento dinamico delle classi, riflessioni, ecc. Tuttavia, la maggior parte dei problemi può essere almeno mitigata. Nell'unico progetto C ++ in cui ho utilizzato i test unitari finora, non abbiamo eseguito i test tutte le volte che avremmo fatto ad es. un progetto Java - ma ancora facevano parte della nostra build CI e li abbiamo trovati utili.

Is the way that Unit Tests require one to structure an application's code "only" beneficial for the unit tests or is it actually beneficial to the applications structure?

Nella mia esperienza, un design verificabile è benefico in generale, non "solo" per i test unitari stessi. Questi vantaggi si presentano su diversi livelli:

  • Rendere testabile la tua progettazione ti obbliga a mettere la tua applicazione in parti piccole, più o meno indipendenti che possono influenzarsi a vicenda solo in modi limitati e ben definiti - questo è molto importante per la stabilità e la manutenzione a lungo termine del tuo programma. Senza questo, il codice tende a deteriorarsi in codice spaghetti in cui qualsiasi modifica apportata in qualsiasi parte della base di codice può causare effetti imprevisti in parti distinte del programma apparentemente non correlate. Il che è, inutile dirlo, l'incubo di ogni programmatore.
  • Scrivere gli stessi test in modo TDD in realtà esercita le API, le classi e i metodi e serve come test molto efficace per rilevare se il tuo progetto ha un senso - se scrivere test contro e interfaccia risulta imbarazzante o difficile, ottieni preziosi feedback iniziali quando è ancora semplice modellare l'API. In altre parole, questo ti impedisce di pubblicare le tue API prematuramente.
  • Il modello di sviluppo applicato da TDD ti aiuta a concentrarti sui compiti concreti da fare e ti tiene in mira, riducendo al minimo le possibilità che tu vada a risolvere altri problemi oltre a quello che dovresti, aggiungendo un extra non necessario caratteristiche e complessità, ecc.
  • Il feedback rapido dei test unitari ti consente di essere audace nel refactoring del codice, permettendoti di adattare e evolvere costantemente il design per tutta la durata del codice, evitando così efficacemente l'entropia del codice.

I remember a rule of thumb that said don't generalize until you need to / until there's a second place that uses the code. With Unit Tests, there's always a second place that uses the code -- namely the unit test. So is this reason enough to generalize?

Se puoi dimostrare che il tuo software fa esattamente quello che dovrebbe fare - e dimostralo in un modo veloce, ripetibile, economico e deterministico per soddisfare i tuoi clienti - senza la generalizzazione o le cuciture "extra" forzate dai test unitari, provaci (e facci sapere come lo fai, perché sono sicuro che molte persone su questo forum sarebbero interessate come me: -)

Come suppongo per "generalizzazione" intendi cose come introdurre un'interfaccia (classe astratta) e polimorfismo invece di una singola classe concreta - se non, per favore chiarisci.

    
risposta data 14.11.2011 - 15:03
fonte
4

Ho intenzione di lanciare The Way of Testivus a te, ma per riassumere:

Se impieghi molto tempo ed energia a rendere il tuo codice più complicato per testare una singola parte del sistema, è possibile che la tua struttura sia sbagliata o che il tuo approccio di testing sia sbagliato.

La guida più semplice è questa: ciò che stai testando è l'interfaccia pubblica del tuo codice nel modo in cui è destinato ad essere utilizzato da altre parti del sistema.

Se i tuoi test stanno diventando lunghi e complicati, è un'indicazione che l'utilizzo dell'interfaccia pubblica sarà difficile.

Se devi utilizzare l'ereditarietà per consentire alla tua classe di essere utilizzata da qualcosa di diverso dalla singola istanza per cui è attualmente in uso, allora ci sono buone probabilità che la tua classe sia troppo legata al suo ambiente di utilizzo. Puoi dare un esempio di una situazione in cui questo è vero?

Tuttavia, fai attenzione ai dogma dei test unitari. Scrivi il test che ti consente di rilevare il problema che ti farà urlare il client .

    
risposta data 14.11.2011 - 18:14
fonte
2

TDD e Unit Testing, è buono per il programma nel suo insieme e non solo per i test unitari. La ragione di ciò è perché fa bene al cervello.

Questa è una presentazione su uno specifico framework ActionScript chiamato RobotLegs. Tuttavia se sfogliate le prime 10 diapositive o giù di lì, inizia ad arrivare alle parti migliori del cervello.

TDD e test delle unità, ti costringe a comportarti in un modo che è meglio per il cervello per elaborare e ricordare le informazioni. Quindi, mentre il tuo compito preciso di fronte a te sta semplicemente facendo un test unitario migliore, o rendendo il codice più unità testabile ... ciò che effettivamente fa è rendere il tuo codice più leggibile, e quindi rendere il tuo codice più gestibile. Ciò ti consente di codificare più velocemente le haith e di comprendere più rapidamente il tuo codice quando devi aggiungere / rimuovere funzionalità, correggere bug o, in generale, aprire il file sorgente.

    
risposta data 14.11.2011 - 15:18
fonte
1

testing the smallest isolatable unit of an application

questo è vero, ma se lo prendi troppo lontano non ti dà molto, e costa molto, e credo che sia questo aspetto che sta promuovendo l'uso del termine BDD per essere ciò che TDD dovrebbe sono stati tutti insieme - la più piccola unità isolabile è ciò che vuoi che sia.

Ad esempio, una volta ho eseguito il debug di una classe di rete che aveva (tra gli altri bit) 2 metodi: 1 per impostare l'indirizzo IP, un altro per impostare il numero di porta. Naturalmente, si trattava di metodi molto semplici e passavano facilmente il test più banale, ma se si imposta il numero di porta e si imposta l'indirizzo ip, non funzionerebbe: l'ip setter stava sovrascrivendo il numero di porta con un valore predefinito. Quindi hai dovuto testare la classe nel suo complesso per garantire un comportamento corretto, qualcosa penso che il concetto di TDD sia mancante ma BDD ti dà. Non è necessario testare ogni singolo metodo, quando è possibile testare l'area più sensibile e più piccola dell'intera applicazione, in questo caso la classe di networking.

In fin dei conti non esiste una bacchetta magica da testare, devi prendere decisioni sensate su quanto e su quale granularità applicare le tue limitate risorse di test. L'approccio basato sugli strumenti che genera automaticamente stub per te non lo fa, è un approccio basato sulla forza smussata.

Quindi, dato questo, non è necessario strutturare il codice in un certo modo per ottenere TDD, ma il livello di test che si raggiunge dipenderà dalla struttura del codice - se si dispone di una GUI monolitica che ha tutta la sua logica strettamente legato alla struttura della GUI, allora troverai più difficile isolare quei pezzi, ma puoi ancora scrivere un test unitario in cui "unità" si riferisce alla GUI e tutto il lavoro del DB di back-end viene deriso. Questo è un esempio estremo, ma mostra che puoi ancora eseguire test automatici su di esso.

Un effetto collaterale della strutturazione del codice per rendere più facile il test delle unità più piccole aiuta a definire meglio l'applicazione e consente di sostituire le parti più facilmente. Aiuta anche quando codifica in quanto sarà meno probabile che 2 sviluppatori lavoreranno sullo stesso componente in un dato momento, a differenza di un'app monolitica che ha dipendenze mescolate che interrompono il lavoro di tutti gli altri in tempi di unione.

    
risposta data 25.07.2012 - 22:55
fonte
0

Hai raggiunto una buona comprensione dei compromessi nella progettazione della lingua. Alcune delle principali decisioni di progettazione in C ++ (il meccanismo di funzione virtuale combinato con il meccanismo di chiamata di una funzione statica) rendono TDD difficile. Il linguaggio in realtà non supporta ciò di cui hai bisogno per renderlo facile. È facile scrivere in C ++ che è praticamente impossibile da testare.

Abbiamo avuto la fortuna di fare il nostro codice TDD C ++ da una mentalità quasi-funzionale: scrivere funzioni non procedure (una funzione che non accetta argomenti e restituisce nulla) e usare la composizione laddove possibile. Dal momento che è difficile sostituire queste classi membro, ci concentriamo a testare tali classi per creare una base affidabile e quindi conoscere le funzioni di base dell'unità quando la aggiungiamo a qualcos'altro.

La chiave è l'approccio quasi-funzionale. Pensaci, se tutto il tuo codice C ++ fosse libero di funzioni che non accedevano a globali, sarebbe un test di unità istantaneo:)

    
risposta data 15.11.2011 - 01:16
fonte

Leggi altre domande sui tag