I test unitari portano a una generalizzazione prematura (in particolare nel contesto del C ++)?


20

Note preliminari

Non entrerò nella distinzione dei diversi tipi di test che ci sono, ci sono già alcune domande su questi siti al riguardo.

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

Il problema dell'isolamento

Qual è la più piccola unità isolabile di un programma. Bene, per come la vedo io (altamente?) Dipende dalla lingua in cui stai codificando.

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

Una cucitura è un luogo in cui è possibile modificare il comportamento nel programma senza modificarlo in quel punto.

E senza entrare nei dettagli, capisco una cucitura - nel contesto del test unitario - per essere 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 in prova di aggiungere più cuciture che sarebbero strettamente richieste per un dato problema.

Esempio:

  • Aggiunta di un'interfaccia virtuale in cui l'implementazione non virtuale sarebbe stata sufficiente
  • Suddivisione - generalizzazione (?) - di una classe (piccola) "solo" per facilitare l'aggiunta di un test.
  • Dividere un progetto eseguibile in libs apparentemente "indipendenti", "solo" per facilitare la loro compilazione indipendente per i test.

La domanda

Proverò alcune versioni che speriamo chiedano lo stesso punto:

  • Il modo in cui i Test di unità richiedono di strutturare il codice di un'applicazione "solo" è utile per i test di unità o è effettivamente vantaggioso per la struttura delle applicazioni.
  • È il codice generalizzazione che è necessario per rendere più unità controllabile utile per qualsiasi cosa , ma i test di unità?
  • L'aggiunta di unit test obbliga a generalizzare inutilmente?
  • La forza dei test delle unità di forma 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 Unit Test, c'è sempre un secondo posto che utilizza il codice, vale a dire il test unitario. Quindi questo motivo è sufficiente per generalizzare?


8
Un meme comune è che qualsiasi modello può essere abusato nel diventare un anti-modello. Lo stesso vale per TDD. È possibile aggiungere interfacce testabili oltre il punto di rendimenti decrescenti, in cui il codice testato è inferiore rispetto alle interfacce di test generalizzate aggiunte, nonché nell'area costi-benefici troppo bassa. Un gioco casual con interfacce aggiunte per test come un sistema operativo di missione nello spazio profondo potrebbe perdere completamente la sua finestra di mercato. Assicurarsi che il test aggiunto sia prima di quei punti di flesso.
hotpaw2,

@ hotpaw2 Blasphemy! :)
maple_shaft

Risposte:


23

Il test unitario - specialmente in C ++ - richiede dal codice in prova di aggiungere più cuciture che sarebbero strettamente richieste per un dato problema.

Solo se non consideri il test come parte integrante della risoluzione dei problemi. 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 secoli da innumerevoli ponti che cadono, esplosioni di automobili, fumo di CPU ecc. Ecc. Cosa stiamo imparando ora nel mondo del software. Tutti creano "cuciture extra" nei loro prodotti per renderli testabili. La maggior parte delle nuove auto al giorno d'oggi dispongono 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 ha scopi diagnostici. Nel mondo dell'hardware, ogni piccola parte di "extra" costa, e quando un prodotto viene prodotto da milioni, questi costi si sommano sicuramente a ingenti somme di denaro. Tuttavia, i produttori sono disposti a spendere tutti questi soldi per la testabilità.

Tornando al mondo del software, il C ++ è davvero più difficile da testare rispetto alle lingue successive con caricamento di classe dinamico, riflessione, ecc. Tuttavia, la maggior parte dei problemi può essere almeno mitigata. Nell'unico progetto C ++ in cui ho usato finora unit test, non abbiamo eseguito i test tutte le volte che avremmo ad esempio un progetto Java, ma comunque facevano parte del nostro build CI e li abbiamo trovati utili.

Il modo in cui i Test unitari richiedono uno per strutturare il codice di un'applicazione "solo" è utile per i test unitari o è effettivamente vantaggioso per la struttura delle applicazioni?

Nella mia esperienza, un progetto verificabile è complessivamente vantaggioso, non "solo" per i test unitari stessi. Questi vantaggi arrivano su diversi livelli:

  • Rendere testabile il tuo progetto ti costringe a dividere 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 manutenibilità a lungo termine del tuo programma. Senza questo, il codice tende a deteriorarsi in spaghetti code in cui qualsiasi modifica apportata in qualsiasi parte della base di codice può causare effetti imprevisti in parti del programma apparentemente non correlate. Inutile dire che è l'incubo di ogni programmatore.
  • Scrivere i test stessi in modo TDD esercita effettivamente le tue API, classi e metodi e serve come test molto efficace per rilevare se il tuo design ha senso - se scrivere test contro e l'interfaccia sembra imbarazzante o difficile, ottieni preziosi feedback iniziali quando ancora facile modellare l'API. In altre parole, ciò ti difende dalla pubblicazione prematura delle API.
  • Il modello di sviluppo applicato da TDD ti aiuta a concentrarti sui compiti concreti da svolgere e ti mantiene mirato, riducendo al minimo le possibilità che tu vaghi per risolvere altri problemi rispetto a quello che dovresti, aggiungendo funzionalità e complessità extra inutili , eccetera.
  • Il rapido feedback dei test unitari ti consente di essere audace nel refactoring del codice, permettendoti di adattare costantemente ed evolvere il design per tutta la durata del codice, prevenendo efficacemente l'entropia del codice.

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 Unit Test, c'è sempre un secondo posto che utilizza il codice, vale a dire il test unitario. Quindi questo motivo è sufficiente per generalizzare?

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

A parte questo, per "generalizzazione" intendi cose come l'introduzione di un'interfaccia (classe astratta) e il polimorfismo invece di una singola classe concreta - in caso contrario, chiarisci.


Signore, ti saluto.
GordonM,

Una nota breve, ma pedante: il "porto diagnostico" è per lo più lì perché i governi li hanno incaricati come parte di un sistema di controllo delle emissioni. Di conseguenza, ha gravi limitazioni; ci sono molte cose che potrebbero essere diagnosticate con questa porta che non lo sono (vale a dire qualcosa che non ha a che fare con il controllo delle emissioni).
Robert Harvey,

4

Sto per lanciarti The Way of Testivus , ma per riassumere:

Se stai spendendo molto tempo ed energia rendendo il tuo codice più complicato per testare una singola parte del sistema, è possibile che la tua struttura sia sbagliata o che il tuo approccio al test sia sbagliato.

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

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

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

Tuttavia, fai attenzione al dogma del test unitario. Scrivi il test che ti consente di rilevare il problema che farà gridare al cliente .


Stavo per aggiungere lo stesso: fare un api, testare l'api, dall'esterno.
Christopher Mahan,

2

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

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

TDD e unit test, ti costringono a comportarti in un modo migliore per il cervello per elaborare e ricordare le informazioni. Quindi, mentre il tuo compito esatto davanti a te è solo fare un unit test migliore, o rendere il codice più unit testabile ... ciò che effettivamente fa è rendere il tuo codice più leggibile, e quindi renderlo più gestibile. Ciò ti consente di codificare gli habbit più velocemente e ti consente di comprendere il tuo codice più rapidamente quando devi aggiungere / rimuovere funzionalità, correggere bug o in generale aprire il file di origine.


1

collaudo dell'unità isolabile più piccola di un'applicazione

questo è vero, ma se lo spingi troppo oltre non ti dà molto, e costa molto, e credo che sia questo aspetto che sta promuovendo l'uso del termine BDD come quello che TDD avrebbe dovuto essere tutto 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, questi erano metodi molto semplici e superavano facilmente il test più banale, ma se si imposta il numero di porta e quindi si imposta l'indirizzo IP, non funzionerebbe - il setter ip stava sovrascrivendo il numero di porta con un valore predefinito. Quindi hai dovuto testare la classe nel suo insieme per garantire un comportamento corretto, qualcosa che penso manchi al concetto di TDD, ma BDD ti dà. Non è necessario testare ogni metodo minuscolo, quando è possibile testare l'area più sensibile e più piccola dell'applicazione complessiva, in questo caso la classe di rete.

Alla fine non c'è nessun proiettile magico da testare, devi prendere decisioni sensate su quanto e su quale granularità applicare le tue risorse di test limitate. L'approccio basato su strumenti che auto - genera stub per te non lo fa, è un approccio a forza smussata.

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

Un effetto collaterale della strutturazione del codice per semplificare il test di unità più piccole aiuta a definire meglio l'applicazione e ciò consente di sostituire più facilmente le parti. Aiuta anche durante la codifica poiché sarà meno probabile che 2 sviluppatori lavoreranno sullo stesso componente in qualsiasi momento - a differenza di un'app monolitica che ha mescolato dipendenze che interrompono il lavoro di tutti gli altri.


0

Hai raggiunto una buona conoscenza dei compromessi nella progettazione del linguaggio. Alcune delle decisioni chiave di progettazione in C ++ (il meccanismo di funzione virtuale mescolato con il meccanismo di chiamata di funzione statica) rendono difficile TDD. La lingua in realtà non supporta ciò di cui hai bisogno per renderlo semplice. È facile scrivere C ++ che è quasi impossibile per test unitari.

Abbiamo avuto più fortuna nel fare il nostro codice C ++ TDD da una mentalità quasi funzionale: scrivere funzioni non procedure (una funzione che non accetta argomenti e restituisce il vuoto), e usare la composizione ovunque possibile. Dal momento che è difficile sostituire queste classi membro, ci concentriamo sul test di tali classi per costruire una base attendibile e quindi sappiamo che l'unità di base funziona quando la aggiungiamo a qualcos'altro.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.