C'è un motivo per cui i test non sono scritti in linea con il codice che testano?


91

Di recente ho letto un po 'di Literate Programming , e mi ha fatto pensare ... I test ben scritti, in particolare le specifiche in stile BDD, possono fare un lavoro migliore nello spiegare cosa fa il codice rispetto alla prosa e hanno il grande vantaggio di verificando la propria accuratezza.

Non ho mai visto test scritti in linea con il codice che testano. È solo perché le lingue non tendono a semplificare la separazione del codice dell'applicazione e del test quando sono scritte nello stesso file sorgente (e nessuno lo ha reso semplice), oppure c'è un motivo più fondato che le persone separano il codice di test dal codice dell'applicazione?


33
Alcuni linguaggi di programmazione come python con doctest ti permettono di farlo.
Simon Bergot,

2
Potresti pensare che le specifiche in stile BDD siano migliori della prosa per spiegare il codice, ma ciò non significa che la combinazione delle due non sia migliore.
JeffO,

5
La metà degli argomenti qui si applica anche alla documentazione incorporata.
CodesInChaos,

3
I doctest di @Simon sono troppo semplicistici per test seri, soprattutto perché non sono progettati per questo. Erano destinati ed eccellono, con esempi di codice nella documentazione che possono essere verificati automaticamente. Ora, alcune persone li usano anche per i test unitari, ma ultimamente (come in, negli anni passati) questo ha richiesto un sacco di difetti, perché tende a finire in pasticci fragili, "documentazione" eccessivamente prolissa e altri pasticci.

7
Design by Contract consente specifiche in linea che semplificano i test.
Fuhrmanator,

Risposte:


89

L'unico vantaggio che mi viene in mente per i test in linea sarebbe la riduzione del numero di file da scrivere. Con gli IDE moderni questo non è un grosso problema.

Esistono tuttavia alcuni ovvi inconvenienti per i test in linea:

  • Viola la separazione delle preoccupazioni . Questo può essere discutibile, ma per me testare la funzionalità è una responsabilità diversa rispetto all'implementazione.
  • Dovresti introdurre nuove funzionalità linguistiche per distinguere tra test / implementazione o rischi di confondere la linea tra i due.
  • I file di origine più grandi sono più difficili da lavorare: più difficili da leggere, più difficili da capire, è più probabile che tu abbia a che fare con conflitti di controllo del codice sorgente.
  • Penso che renderebbe più difficile indossare il cappello "tester", per così dire. Se stai osservando i dettagli dell'implementazione, sarai più tentato di saltare l'implementazione di determinati test.

9
Interessante. Immagino che il vantaggio che vedo sia che quando hai il tuo cappello da "programmatore", vuoi pensare ai test, ma è un buon punto che non è vero il contrario.
Chris Devereux,

2
Lungo queste linee, è possibile (e forse desiderabile) avere una persona che crea i test e una seconda che implementa effettivamente il codice. Mettere i test in linea rende questo più difficile.
Jim Nutt,

6
declasserei se potessi. In che modo questa è una risposta? Gli implementatori non scrivono test? Le persone saltano i test se guardano i dettagli di implementazione? "Troppo difficile" Conflitti su file di grandi dimensioni ?? E in che modo un test potrebbe essere confuso con un dettaglio di implementazione ???
bharal,

5
@bharal Inoltre, scritto su "Troppo difficile", il masochismo è una virtù folle. Voglio che tutto sia facile, tranne per il problema che sto effettivamente cercando di risolvere.
Deworde,

3
Il test unitario può essere considerato documentazione. Ciò suggerisce che i test unitari dovrebbero essere inclusi nel codice per lo stesso motivo dei commenti - per migliorare la leggibilità. Tuttavia, il problema è che ci sono molti test unitari e un sacco di overhead di implementazione dei test che non specifica i risultati previsti. Anche i commenti all'interno del codice dovrebbero essere concisi, con spiegazioni più ampie spostate di mezzo: in un blocco di commenti esterno alla funzione, in un file separato o forse in un documento di progettazione. I test unitari sono IMO raramente se mai abbastanza brevi da mantenere il codice testato come commenti.
Steve314,

36

Posso pensare ad alcuni:

  • Leggibilità. L'inversione di codice e test "reali" renderà più difficile la lettura del codice reale.

  • Codice gonfio. Mescolare codice "reale" e codice di prova negli stessi file / classi / qualunque cosa risulti in file compilati più grandi, ecc. Ciò è particolarmente importante per le lingue con associazione tardiva.

  • Potresti non volere che i tuoi clienti / clienti vedano il tuo codice di prova. (Non mi piace questo motivo ... ma se stai lavorando a un progetto a sorgente chiuso, è improbabile che il codice di test possa aiutare il cliente.)

Ora ci sono possibili soluzioni per ciascuno di questi problemi. Ma IMO, è più semplice non andarci in primo luogo.


Vale la pena osservare che ai primi tempi i programmatori Java erano soliti fare questo tipo di cose; ad esempio includendo un main(...)metodo in una classe per facilitare i test. Questa idea è quasi completamente scomparsa. È prassi del settore implementare i test separatamente utilizzando un framework di test di qualche tipo.

Vale anche la pena osservare che la programmazione letterata (come concepita da Knuth) non ha mai preso piede nel settore dell'ingegneria del software.


4
+1 Problemi di leggibilità: il codice di test potrebbe essere proporzionalmente maggiore del codice di implementazione, specialmente nei progetti OO.
Fuhrmanator,

2
+1 per la segnalazione mediante framework di test. Non riesco a immaginare di utilizzare un buon framework di test contemporaneamente al codice di produzione.
joshin4colours,

1
RE: Potresti non volere che i tuoi clienti / clienti vedano il tuo codice di prova. (Non mi piace questo motivo ... ma se stai lavorando a un progetto a sorgente chiuso, è improbabile che il codice di test possa aiutare il cliente.) - Potrebbe essere desiderabile eseguire i test sulla macchina client. L'esecuzione dei test può aiutare a identificare rapidamente qual è il problema e a identificare le differenze nell'ambiente del client.
sixtyfootersdude,

1
@sixtyfootersdude - questa è una situazione abbastanza insolita. E supponendo che stavi sviluppando un sistema chiuso, non vorrai includere i tuoi test nella distro binaria standard per ogni evenienza. (Dovresti creare un pacchetto separato contenente i test che desideri vengano eseguiti dal cliente.)
Stephen C,

1
1) Ti sei perso la prima parte della mia risposta in cui ho fornito tre motivi reali? C'era un po 'di "pensiero critico" coinvolto lì .... 2) Ti sei perso la seconda parte in cui ho detto che i programmatori Java lo facevano, ma ora non lo fanno? E l'ovvia conseguenza che i programmatori hanno smesso di farlo ... per una buona ragione?
Stephen C,

14

In realtà, puoi pensare a Design by Contract come a farlo. Il problema è che la maggior parte dei linguaggi di programmazione non ti consente di scrivere codice in questo modo :( È molto facile testare manualmente le precondizioni, ma le condizioni dei post sono una vera sfida senza cambiare il modo in cui scrivi il codice (un enorme IMO negativo).

Michael Feathers ha una presentazione a riguardo e questo è uno dei tanti modi in cui afferma che puoi migliorare la qualità del codice.


13

Per molte delle stesse ragioni che si tenta di evitare l'accoppiamento stretto tra le classi nel codice, è anche una buona idea evitare l'accoppiamento non necessario tra test e codice.

Creazione: test e codice possono essere scritti in momenti diversi, da persone diverse.

Controllo: se i test vengono utilizzati per specificare i requisiti, vorresti sicuramente che fossero soggetti a regole diverse su chi può modificarli e quando rispetto al codice effettivo.

Riusabilità: se si mettono in linea i test, non è possibile utilizzarli con un altro codice.

Immagina di avere un pezzo di codice che fa il lavoro correttamente, ma lascia molto a desiderare in termini di prestazioni, manutenibilità, qualunque cosa. Decidi di sostituire quel codice con un codice nuovo e migliorato. L'uso dello stesso set di test può aiutarti a verificare che il nuovo codice produca gli stessi risultati del vecchio codice.

Selezionabilità: mantenere i test separati dal codice semplifica la scelta dei test che si desidera eseguire.

Ad esempio, potresti avere una piccola suite di test che si riferiscono solo al codice su cui stai attualmente lavorando e una suite più grande che testa l'intero progetto.


Sono perplesso sulle tue ragioni: TDD dice già che la creazione del test avviene prima (o allo stesso tempo) del codice di produzione e deve essere eseguita dallo stesso programmatore! Indicano anche che i test sono praticamente dei requisiti. Naturalmente, queste obiezioni non si applicano se non ti iscrivi al dogma TDD (il che sarebbe accettabile, ma è necessario chiarirlo!). Inoltre, cos'è esattamente un test "riutilizzabile"? I test non sono, per definizione, specifici del codice che testano?
Andres F.

1
@AndresF. No, i test non sono specifici del codice che testano; sono specifici per il comportamento che testano. Quindi, supponiamo di avere un modulo Widget completo di una serie di test che verificano che il Widget si stia comportando correttamente. Il tuo collega presenta BetterWidget, che pretende di fare la stessa cosa di Widget ma tre volte più veloce. Se i test per Widget sono incorporati nel codice sorgente del Widget nello stesso modo in cui Literate Programming incorpora la documentazione nel codice sorgente, non è possibile applicare molto bene tali test a BetterWidget per verificare che si comporti allo stesso modo di Widget.
Caleb,

@AndresF. non è necessario specificare di non seguire TDD. non è un default cosmico. Per quanto riguarda il punto di riutilizzo. Quando collaudi un sistema ti preoccupi degli ingressi e delle uscite, non degli interni. Quando è quindi necessario creare un nuovo sistema che si comporti esattamente come questo ma sia implementato in modo diverso, è bello avere dei test che è possibile eseguire sia sul vecchio che sul nuovo sistema. questo mi è successo più di una volta, a volte è necessario lavorare sul nuovo sistema mentre il vecchio è ancora in fase di produzione o addirittura eseguirli fianco a fianco. guarda come Facebook stava testando la "fibra reattiva" con i test di reazione per raggiungere la parità.
user1852503

10

Ecco alcuni altri motivi a cui posso pensare:

  • avere test in una libreria separata rende più semplice collegare solo quella libreria al framework di test e non al codice di produzione (questo potrebbe essere evitato da alcuni preprocessori, ma perché costruire una cosa del genere quando la soluzione più semplice è scrivere i test in un posto separato)

  • i test di una funzione, una classe, una libreria sono generalmente scritti dal punto di vista degli "utenti" (un utente di quella funzione / classe / libreria). Tale "utilizzo del codice" è in genere scritto in un file o una libreria separati e un test può essere più chiaro o "più realistico" se imita quella situazione.


5

Se i test fossero in linea, sarebbe necessario rimuovere il codice necessario per i test quando si spedisce il prodotto al cliente. Quindi, un posto in più in cui si memorizzano i test separa semplicemente tra il codice che si serve e il codice vostro cliente ha bisogno.


9
Non impossibile. Richiederebbe una fase di preelaborazione aggiuntiva, proprio come LP. Potrebbe essere fatto facilmente in C, o in un linguaggio di compilazione in js, per esempio.
Chris Devereux,

+1 per avermelo fatto notare. Ho modificato la mia risposta per rappresentarla.
mhr

C'è anche un presupposto che la dimensione del codice sia importante in ogni caso. Solo perché è importante in alcuni casi non significa che sia importante in tutti i casi. Esistono molti ambienti in cui i programmatori non sono guidati per ottimizzare le dimensioni del codice sorgente. Se così fosse, non creerebbero così tante classi.
zumalifeguard,

5

Questa idea equivale semplicemente a un metodo "Self_Test" nel contesto di una progettazione basata sugli oggetti o orientata agli oggetti. Se si utilizza un linguaggio compilato basato su oggetti come Ada, tutto il codice di autotest verrà contrassegnato dal compilatore come inutilizzato (mai invocato) durante la compilazione della produzione, e quindi sarà tutto ottimizzato via - nessuno di questi verrà visualizzato nella eseguibile risultante.

Usare un metodo "Self_Test" è un'ottima idea, e se i programmatori fossero davvero interessati alla qualità, lo farebbero tutti. Una questione importante, tuttavia, è che il metodo "Self_Test" deve avere un'intensa disciplina, in quanto non può accedere a nessuno dei dettagli di implementazione e deve invece fare affidamento solo su tutti gli altri metodi pubblicati nelle specifiche dell'oggetto. Ovviamente, se l'autotest fallisce, l'implementazione dovrà cambiare. L'autotest dovrebbe testare rigorosamente tutte le proprietà pubblicate dei metodi dell'oggetto, ma non fare mai affidamento in alcun modo su alcun dettaglio di un'implementazione specifica.

I linguaggi basati sugli oggetti e orientati agli oggetti forniscono spesso esattamente quel tipo di disciplina rispetto ai metodi esterni all'oggetto testato (impongono la specifica dell'oggetto, impedendo qualsiasi accesso ai suoi dettagli di implementazione e generando un errore di compilazione se viene rilevato un simile tentativo ). Ma ai metodi interni dell'oggetto viene dato l'accesso completo a ogni dettaglio di implementazione. Quindi il metodo di autotest si trova in una situazione unica: deve essere un metodo interno a causa della sua natura (l'autotest è ovviamente un metodo dell'oggetto testato), ma deve ricevere tutta la disciplina del compilatore di un metodo esterno ( deve essere indipendente dai dettagli di implementazione dell'oggetto). Pochi linguaggi di programmazione offrono la capacità di disciplinare un oggetto " s metodo interno come se fosse un metodo esterno. Quindi questo è un importante problema di progettazione del linguaggio di programmazione.

In assenza di un adeguato supporto del linguaggio di programmazione, il modo migliore per farlo è creare un oggetto compagno. In altre parole, per ogni oggetto che codifichi (chiamiamolo "Big_Object"), crei anche un secondo oggetto associato il cui nome è costituito da un suffisso standard concatenato con il nome dell'oggetto "reale" (in questo caso "Big_Object_Self_Test ") e la cui specifica è costituita da un singolo metodo (" Big_Object_Self_Test.Self_Test (This_Big_Object: Big_Object) restituisce Boolean; "). L'oggetto associato dipenderà quindi dalle specifiche dell'oggetto principale e il compilatore applicherà completamente tutta la disciplina di tale specifica contro l'implementazione dell'oggetto associato.


4

Questo è in risposta a un gran numero di commenti che suggeriscono che i test in linea non vengono eseguiti perché è difficile rimuovere il codice di test dai build di rilascio. Questo non è vero. Quasi tutti i compilatori e gli assemblatori lo supportano già, con linguaggi compilati come C, C ++, C #, questo viene fatto con quelle che vengono chiamate direttive del compilatore.

Nel caso di c # (credo anche c ++, la sintassi potrebbe essere leggermente diversa a seconda del compilatore che stai usando), ecco come puoi farlo.

#define DEBUG //  = true if c++ code
#define TEST /* can also be defined in the make file for c++ or project file for c# and applies to all associated .cs/.cpp files */

//somewhere in your code
#if DEBUG
// debug only code
#elif TEST
// test only code
#endif

Poiché utilizza le direttive del compilatore, il codice non esisterà nei file eseguibili creati se i flag non sono impostati. Questo è anche il modo in cui programmi "scrivi una volta, compila due volte" per più piattaforme / hardware.


2

Utilizziamo test in linea con il nostro codice Perl. C'è un modulo, Test :: Inline , che genera file di test dal codice inline.

Non sono particolarmente bravo a organizzare i miei test e li ho trovati più facili e più probabilità di essere mantenuti quando sono in linea.

Rispondere a un paio di preoccupazioni sollevate:

  • I test incorporati sono scritti nelle sezioni POD, quindi non fanno parte del codice effettivo. Vengono ignorati dall'interprete, quindi non c'è un eccesso di codice.
  • Usiamo il pieghevole Vim per nascondere le sezioni del test. L'unica cosa che vedi è una singola riga sopra ogni metodo testato come +-- 33 lines: #test----. Quando si desidera lavorare con il test, è sufficiente espanderlo.
  • Il modulo Test :: Inline "compila" i test in normali file compatibili con TAP, in modo che possano coesistere con i test tradizionali.

Per riferimento:


1

Erlang 2 in realtà supporta i test in linea. Qualsiasi espressione booleana nel codice che non viene utilizzata (ad esempio assegnata a una variabile o passata) viene automaticamente trattata come un test e valutata dal compilatore; se l'espressione è falsa, il codice non viene compilato.


1

Un altro motivo per separare i test è che spesso si utilizzano librerie aggiuntive o addirittura diverse per i test rispetto all'implementazione effettiva. Se mescoli test e implementazione, il compilatore non può catturare l'utilizzo accidentale delle librerie di test nell'implementazione.

Inoltre, i test tendono ad avere molte più righe di codice rispetto alle parti di implementazione che testano, quindi avrai difficoltà a trovare l'implementazione tra tutti i test. :-)


0

Questo non è vero. È molto meglio posizionare i test unitari accanto al codice di produzione quando il codice di produzione, specialmente quando la routine di produzione è pura.

Se, ad esempio, stai sviluppando in .NET, puoi inserire il codice di test nell'assieme di produzione e quindi utilizzare Scalpel per rimuoverlo prima della spedizione.

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.