Test vs Don't Repeat Yourself (DRY)


11

Perché ripetere te stesso scrivendo test è così fortemente incoraggiato?

Sembra che i test esprimano sostanzialmente la stessa cosa del codice, e quindi è un duplicato (nel concetto, non implementazione) del codice. L'obiettivo finale di DRY non sarebbe l'eliminazione di tutto il codice di test?

Risposte:


24

Credo che questo sia un malinteso in qualsiasi modo mi venga in mente.

Il codice di test che verifica il codice di produzione non è affatto simile. Dimostrerò in Python:

def multiply(a, b):
    """Multiply ``a`` by ``b``"""
    return a*b

Quindi un semplice test sarebbe:

def test_multiply():
    assert multiply(4, 5) == 20

Entrambe le funzioni hanno una definizione simile ma entrambe fanno cose molto diverse. Nessun codice duplicato qui. ;-)

Si verifica inoltre che le persone scrivano test duplicati essenzialmente con un'asserzione per funzione di test. Questa è follia e ho visto persone farlo. Questa è una cattiva pratica.

def test_multiply_1_and_3():
    """Assert that a multiplication of 1 and 3 is 3."""
    assert multiply(1, 3) == 3

def test_multiply_1_and_7():
    """Assert that a multiplication of 1 and 7 is 7."""
    assert multiply(1, 7) == 7

def test_multiply_3_and_4():
    """Assert that a multiplication of 3 and 4 is 12."""
    assert multiply(3, 4) == 12

Immagina di farlo per oltre 1000 righe di codice efficaci. Al contrario, esegui il test in base alla "funzione":

def test_multiply_positive():
    """Assert that positive numbers can be multiplied."""
    assert multiply(1, 3) == 3
    assert multiply(1, 7) == 7
    assert multiply(3, 4) == 12

def test_multiply_negative():
    """Assert that negative numbers can be multiplied."""
    assert multiply(1, -3) == -3
    assert multiply(-1, -7) == 7
    assert multiply(-3, 4) == -12

Ora, quando le funzionalità vengono aggiunte / rimosse, devo solo considerare di aggiungere / rimuovere una funzione di test.

Potresti aver notato che non ho applicato i forloop. Questo perché ripetere alcune cose è buono. Quando avrei applicato i loop, il codice sarebbe molto più breve. Ma quando un'asserzione fallisce potrebbe offuscare l'output mostrando un messaggio ambiguo. In questo caso, i test saranno meno utili e sarà necessario un debugger per verificare dove le cose vanno male.


8
Un'asserzione per test è tecnicamente consigliata perché significa che più problemi non verranno visualizzati come un singolo errore. Tuttavia, in pratica, penso che un'attenta aggregazione delle asserzioni riduca la quantità di codice ripetuto e non mi attengo quasi mai a un'affermazione per linea guida del test.
Rob Church,

@ pink-diamond-square Vedo che NUnit non interrompe i test dopo che un'asserzione fallisce (cosa che penso sia strana). In quel caso specifico è davvero meglio avere un'asserzione per test. Se un framework di unit test interrompe i test dopo un'asserzione fallita, sono più asserzioni multiple.
siebz0r

3
NUnit non interrompe l'intera suite di test, ma quel test si interrompe a meno che non si adottino misure per impedirlo (è possibile rilevare l'eccezione generata, che è talvolta utile). Il punto che penso stiano sollevando è che se si scrivono test che includono più di un'asserzione, non si otterranno tutte le informazioni necessarie per correggere il problema. Per elaborare il tuo esempio, immagina che questa funzione di moltiplicazione non gradisca il numero 3. In questo caso, assert multiply(1,3)fallirebbe ma non otterrai anche il rapporto di test fallito assert multiply(3,4).
Rob Church,

Ho solo pensato di sollevarlo perché una singola asserzione per test è, da quello che ho letto nel mondo .net, la "buona pratica" e asserzioni multiple è "uso pragmatico". Sembra un po 'diverso nella documentazione di Python in cui l'esempio def test_shuffleesegue due asserzioni.
Rob Church,

Sono d'accordo e in disaccordo: D C'è chiaramente una ripetizione qui: in assert multiply(*, *) == *modo da poter definire una assert_multiplyfunzione. Nello scenario attuale non importa dal conteggio delle righe e dalla leggibilità, ma dai test più lunghi è possibile riutilizzare asserzioni complicate, dispositivi, codice di generazione di dispositivi, ecc ... Non so se questa è una buona pratica, ma di solito lo faccio Questo.
inf3rno,

10

Sembra che i test esprimano sostanzialmente la stessa cosa del codice, e quindi è un duplicato

No, questo non è vero.

I test hanno uno scopo diverso rispetto all'implementazione:

  • I test assicurano che l'implementazione funzioni.
  • Servono da documentazione: guardando i test, vedi i contratti che il tuo codice deve soddisfare, cioè quali input restituiscono quali output, quali sono i casi speciali ecc.
  • Inoltre, i test garantiscono che quando si aggiungono nuove funzionalità, la funzionalità esistente non si interrompe.

4

No. DRY riguarda la scrittura del codice solo una volta per eseguire una determinata attività, i test sono la conferma che l'attività è stata eseguita correttamente. È in qualche modo simile a un algoritmo di voto, dove ovviamente usare lo stesso codice sarebbe inutile.


2

L'obiettivo finale di DRY non sarebbe l'eliminazione di tutto il codice di test?

No, l'obiettivo finale di DRY significherebbe in realtà l'eliminazione di tutto il codice di produzione .

Se i nostri test potessero essere le specifiche perfette di ciò che vogliamo che faccia il sistema, dovremmo semplicemente generare automaticamente il corrispondente codice di produzione (o binari), rimuovendo efficacemente la base di codice di produzione in sé.

Questo è in realtà ciò che pretendono di raggiungere approcci come l'architettura guidata dai modelli: un'unica fonte di verità progettata dall'uomo da cui tutto deriva dal calcolo.

Non penso che il contrario (sbarazzarsi di tutti i test) sia desiderabile perché:

  • È necessario risolvere la discrepanza di impedenza tra implementazione e specifica. Il codice di produzione può trasmettere l'intento in una certa misura, ma non sarà mai facile ragionare su test ben espressi. Noi esseri umani abbiamo bisogno di una visione più elevata del perché stiamo costruendo cose. Anche se non fai test a causa del DRY, le specifiche dovranno comunque essere scritte nei documenti, il che è una bestia decisamente più pericolosa in termini di disadattamento dell'impedenza e desincronizzazione del codice se me lo chiedi.
  • Mentre il codice di produzione è probabilmente facilmente derivabile dalle specifiche eseguibili corrette (presupponendo un tempo sufficiente), una suite di test è molto più difficile da ricostituire dal codice finale di un programma. Le specifiche non appaiono chiaramente solo guardando il codice, perché le interazioni tra le unità di codice in fase di esecuzione sono difficili da capire. Questo è il motivo per cui abbiamo difficoltà a gestire applicazioni legacy senza test. In altre parole: se vuoi che la tua applicazione sopravviva per più di qualche mese, sarebbe meglio perdere il disco rigido che ospita la tua base di codice di produzione rispetto a quello in cui si trova la tua suite di test.
  • È molto più facile introdurre un errore per errore nel codice di produzione che nel codice di prova. E poiché il codice di produzione non si verifica da solo (sebbene ciò possa essere affrontato con Design by Contract o sistemi di tipo più ricco), abbiamo ancora bisogno di qualche programma esterno per testarlo e avvisarci se si verifica una regressione.

1

Perché a volte ripetersi va bene. Nessuno di questi principi deve essere preso in ogni circostanza senza domande o contesti. A volte ho scritto dei test contro una versione ingenua (e lenta) di un algoritmo, che è una violazione abbastanza chiara di DRY, ma sicuramente utile.


1

Poiché il test unitario consiste nel rendere più difficili le modifiche involontarie , a volte può rendere anche le modifiche intenzionali più difficili. Questo fatto è effettivamente correlato al principio DRY.

Ad esempio, se si dispone di una funzione MyFunctionche viene chiamata nel codice di produzione in un solo posto e si scrivono 20 unità di test per essa, si può facilmente finire per avere 21 posizioni nel codice in cui viene chiamata quella funzione. Ora, quando devi cambiare la firma MyFunction, la semantica o entrambi (perché alcuni requisiti cambiano), hai 21 posti da cambiare invece di uno solo. E la ragione è davvero una violazione del principio DRY: hai ripetuto (almeno) la stessa chiamata di funzione a MyFunction21 volte.

L'approccio corretto per un caso del genere è applicare il principio DRY anche al codice di test: durante la scrittura di 20 unit test, incapsulare le chiamate MyFunctionnei test unitari in poche funzioni di supporto (idealmente solo una), che vengono utilizzate dal 20 test unitari. Idealmente, si ottiene solo due posizioni nel codice che chiama MyFunction: una dal codice di produzione e una dai test unitari. Quindi, quando devi cambiare la firma in un MyFunctionsecondo momento, avrai solo pochi punti da cambiare nei tuoi test.

"Pochi posti" sono ancora più di "un posto" (ciò che si ottiene senza test unitari), ma i vantaggi di avere test unitari dovrebbero superare notevolmente il vantaggio di avere meno codice da modificare (altrimenti si esegue completamente il test unitario sbagliato).


0

Una delle maggiori sfide per la creazione di software è l'acquisizione dei requisiti; vale a dire rispondere alla domanda "cosa dovrebbe fare questo software?" Il software ha bisogno di requisiti precisi per definire con precisione ciò che il sistema deve fare, ma quelli che definiscono le esigenze di sistemi e progetti software spesso includono persone che non hanno un background software o formale (matematico). La mancanza di rigore nella definizione dei requisiti ha costretto lo sviluppo del software a trovare un modo per convalidare il software ai requisiti.

Il team di sviluppo si è trovato a tradurre la descrizione colloquiale di un progetto in requisiti più rigorosi. La disciplina dei test si è consolidata come il punto di controllo per lo sviluppo del software, per colmare il divario tra ciò che un cliente dice di voler e ciò che il software capisce di voler. Sia gli sviluppatori di software che il team di qualità / test formano la comprensione delle specifiche (informali) e ogni (indipendentemente) scrive software o test per assicurarsi che la loro comprensione sia conforme. L'aggiunta di un'altra persona per comprendere i requisiti (imprecisi) ha aggiunto domande e prospettive diverse per affinare ulteriormente la precisione dei requisiti.

Dato che ci sono sempre stati test di accettazione, è stato naturale espandere il ruolo di test per scrivere test automatici e unitari. Il problema era che significava assumere programmatori per fare dei test, e quindi hai ristretto la prospettiva dalla garanzia della qualità ai programmatori che facevano i test.

Detto questo, probabilmente stai facendo dei test sbagliati se i tuoi test differiscono poco dai programmi reali. Il suggerimento di Msdy sarebbe quello di concentrarsi maggiormente su cosa nei test e meno su come.

L'ironia è che, anziché acquisire una specifica formale dei requisiti dalla descrizione colloquiale, l'industria ha scelto di implementare test puntuali come codice per automatizzare i test. Invece di produrre requisiti formali a cui il software potrebbe essere costruito per rispondere, l'approccio adottato è stato quello di testare alcuni punti, piuttosto che avvicinarsi alla costruzione di software usando la logica formale. Questo è un compromesso, ma è stato abbastanza efficace e relativamente efficace.


0

Se ritieni che il tuo codice di test sia troppo simile al tuo codice di implementazione, ciò potrebbe indicare che stai utilizzando eccessivamente un framework di derisione. I test basati su simulazioni a un livello troppo basso possono finire con l'impostazione del test che assomiglia molto al metodo testato. Prova a scrivere test di livello superiore che hanno meno probabilità di interrompersi se cambi l'implementazione (so che può essere difficile, ma se riesci a gestirlo avrai come risultato una suite di test più utile).


0

I test unitari non dovrebbero includere una duplicazione del codice in prova, come è già stato notato.

Vorrei aggiungere, tuttavia, che i test unitari in genere non sono così ASCIUTTI come il codice "produzione", perché l'installazione tende ad essere simile (ma non identica) tra i test ... specialmente se si dispone di un numero significativo di dipendenze che si stanno prendendo in giro / fingendo.
È ovviamente possibile trasformare questo genere di cose in un metodo di configurazione comune (o in una serie di metodi di configurazione) ... ma ho scoperto che quei metodi di configurazione tendono ad avere lunghi elenchi di parametri ed essere piuttosto fragili.

Quindi sii pragmatico. Se riesci a consolidare il codice di installazione senza compromettere la manutenibilità, fallo sicuramente. Ma se l'alternativa è un insieme complesso e fragile di metodi di installazione, un po 'di ripetizione nei metodi di prova è OK.

Un evangelista locale TDD / BDD dice così:
"Il tuo codice di produzione dovrebbe essere ASCIUTTO. Ma va bene che i tuoi test siano" umidi "."


0

Sembra che i test esprimano sostanzialmente la stessa cosa del codice, e quindi è un duplicato (nel concetto, non implementazione) del codice.

Questo non è vero, i test descrivono i casi d'uso, mentre il codice descrive un algoritmo che passa i casi d'uso, quindi più generale. Con TDD inizi a scrivere casi d'uso (probabilmente basati sulla storia dell'utente) e successivamente a implementare il codice necessario per passare questi casi d'uso. Quindi scrivi un piccolo test, un piccolo pezzo di codice, e successivamente rifatti se necessario per sbarazzarti delle ripetizioni. Funziona così.

Dai test possono esserci anche ripetizioni. Ad esempio è possibile riutilizzare gli apparecchi, il codice di generazione degli apparecchi, le asserzioni complicate, ecc ... Di solito lo faccio, per evitare bug nei test, ma di solito dimentico di provare prima se un test fallisce davvero e può davvero rovinare la giornata , quando stai cercando il bug nel codice per mezz'ora e il test è sbagliato ... xD

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.