Come funziona il test unitario?

23

Sto cercando di rendere il mio codice più robusto e ho letto dei test unitari, ma trovo molto difficile trovare un uso utile. Ad esempio, l' esempio di Wikipedia :

public class TestAdder {
    public void testSum() {
        Adder adder = new AdderImpl();
        assert(adder.add(1, 1) == 2);
        assert(adder.add(1, 2) == 3);
        assert(adder.add(2, 2) == 4);
        assert(adder.add(0, 0) == 0);
        assert(adder.add(-1, -2) == -3);
        assert(adder.add(-1, 1) == 0);
        assert(adder.add(1234, 988) == 2222);
    }
}

Ritengo che questo test sia totalmente inutile, perché ti viene richiesto di calcolare manualmente il risultato desiderato e testarlo, mi sento come se un test unitario migliore sarebbe

assert(adder.add(a, b) == (a+b));

ma questo è solo il codice della funzione stessa nel test. Qualcuno può fornirmi un esempio in cui il test unitario è effettivamente utile? Per la precisione sto codificando per lo più funzioni "procedurali" che richiedono circa 10 booleani e pochi int e mi danno un risultato int basato su questo, mi sento come l'unica unità di test che potrei fare sarebbe semplicemente ricodificare l'algoritmo nel test. modificare: Dovrei anche averlo precistato mentre eseguivo il porting (possibilmente mal progettato) codice ruby (che non ho creato)

    
posta lezebulon 29.12.2011 - 17:29
fonte

13 risposte

26

I test unitari, se stai testando unità abbastanza piccole, stanno sempre asserendo ciò che è chiaramente ovvio.

La ragione per cui add(x, y) viene menzionata anche per un test di unità, è perché qualche tempo dopo qualcuno entrerà in add e metterà codice di gestione della logica fiscale speciale non rendendosi conto che l'aggiunta è usata ovunque.

I test unitari sono molto molto sul principio associativo: se A fa B, e B fa C, allora A fa C. "A fa C" è un test di livello superiore. Ad esempio, considera il seguente codice aziendale completamente legittimo:

public void LoginUser (string username, string password) {
    var user = db.FetchUser (username);

    if (user.Password != password)
        throw new Exception ("invalid password");

    var roles = db.FetchRoles (user);

    if (! roles.Contains ("member"))
        throw new Exception ("not a member");

    Session["user"] = user;
}

A prima vista questo sembra un metodo fantastico per il test dell'unità, perché ha uno scopo molto chiaro. Tuttavia, fa circa 5 cose diverse. Ogni cosa ha un caso valido e invalido e farà un'enorme permutazione dei test unitari. Idealmente questo è ulteriormente suddiviso:

public void LoginUser (string username, string password) {

    var user = _userRepo.FetchValidUser (username, password);

    _rolesRepo.CheckUserForRole (user, "member");

    _localStorage.StoreValue ("user", user);
}

Ora siamo giù alle unità. A un test di unità non interessa cosa _userRepo considera comportamento valido per FetchValidUser , solo che è chiamato. È possibile utilizzare un altro test per garantire esattamente ciò che costituisce un utente valido. Allo stesso modo per CheckUserForRole ... hai disaccoppiato il test dal sapere come si presenta la struttura del ruolo. Hai anche disaccoppiato l'intero programma dall'essere strettamente legato a Session . Immagino che tutti i pezzi mancanti qui assomiglieranno:

class UserRepository : IUserRepository
{
    public User FetchValidUser (string username, string password)
    {
        var user = db.FetchUser (username);

        if (user.Password != password)
            throw new Exception ("invalid password");

        return user;
    }
}

class RoleRepository : IRoleRepository
{
    public void CheckUserForRole (User user, string role)
    {
        var roles = db.FetchRoles (user);

        if (! roles.Contains (role))
            throw new Exception ("not a member");
    }
}

class SessionStorage : ILocalStorage
{
    public void StoreValue (string key, object value)
    {
        Session[key] = value;
    }
}

Con il refactoring hai compiuto diverse cose contemporaneamente. Il programma è molto più utile per strappare le strutture sottostanti (è possibile abbandonare il livello database per NoSQL) o aggiungere il blocco senza soluzione di continuità una volta realizzato Session non è thread-safe o qualsiasi altra cosa. Ti sei anche procurato test molto semplici da scrivere per queste tre dipendenze.

Spero che questo aiuti:)

    
risposta data 29.12.2011 - 20:54
fonte
13

I am currently coding mostly "procedural" functions that take ~10 booleans and a few ints and give me an int result based on this, I feel like the only unit testing I could do would be to simply re-code the algorithm in the test

Sono abbastanza sicuro che ciascuna delle tue funzioni procedurali è deterministica, quindi restituisce un risultato int specifico per ogni set di valori di input. Idealmente, avresti una specifica funzionale da cui puoi capire quale risultato dovresti ricevere per determinati insiemi di valori di input. In mancanza di ciò, è possibile eseguire il codice ruby (che si presume funzioni correttamente) per determinati set di valori di input e registrare i risultati. Quindi, è necessario HARD CODE i risultati nel test. Il test dovrebbe essere una dimostrazione che il tuo codice produce effettivamente risultati che sono noti per essere corretti .

    
risposta data 29.12.2011 - 19:06
fonte
12

Poiché nessun altro sembra aver fornito un esempio reale:

    public void testRoman() {
        RomanNumeral numeral = new RomanNumeral();
        assert( numeral.toRoman(1) == "I" )
        assert( numeral.toRoman(4) == "IV" )
        assert( numeral.toRoman(5) == "V" )
        assert( numeral.toRoman(9) == "IX" )
        assert( numeral.toRoman(10) == "X" )
    }
    public void testSqrt() {
        assert( sqrt(4) == 2 )
        assert( sqrt(9) == 3 )
    }

Dici:

I feel that this test is totally useless, because you are required to manually compute the wanted result and test it

Ma il punto è che è molto meno probabile che tu commetta un errore (o almeno più probabile che noti i tuoi errori) quando esegui i calcoli manuali e poi durante la codifica.

Quanto è probabile che tu commetta un errore nel codice di conversione da decimale a romano? Abbastanza probabile Quanto è probabile che tu commetta un errore quando converti i numeri decimali in numeri romani a mano? Non molto probabile. Ecco perché testiamo contro i calcoli manuali.

Quanto è probabile che tu commetta un errore quando implementi una funzione radice quadrata? Abbastanza probabile Quanto è probabile che tu commetta un errore nel calcolo di una radice quadrata a mano? Probabilmente più probabile. Ma con sqrt, puoi usare una calcolatrice per ottenere le risposte.

FYI I am currently coding mostly "procedural" functions that take ~10 booleans and a few ints and give me an int result based on this, I feel like the only unit testing I could do would be to simply re-code the algorithm in the test

Quindi ho intenzione di speculare su cosa sta succedendo qui. Le tue funzioni sono un po 'complicate, quindi è difficile capire dagli input quale dovrebbe essere l'output. Per fare ciò, devi eseguire manualmente (nella tua testa) la funzione per capire quale sia l'output. Comprensibilmente, sembra un po 'inutile e soggetto a errori.

La chiave è che vuoi trovare le uscite corrette. Ma devi testare quelle uscite contro qualcosa che si sa essere corretto. Non è bene scrivere il proprio algoritmo per calcolarlo, perché potrebbe essere errato. In questo caso è troppo difficile calcolare manualmente i valori.

Tornerei al codice ruby ed eseguo queste funzioni originali con vari parametri. Prenderò i risultati del codice rubino e inserirò quelli nel test unitario. In questo modo non devi fare il calcolo manuale. Ma stai testando il codice originale. Questo dovrebbe aiutare a mantenere i risultati allo stesso modo, ma se ci sono errori nell'originale, questo non ti aiuterà. Fondamentalmente, puoi trattare il codice originale come la calcolatrice nell'esempio sqrt.

Se hai mostrato il codice reale che stai trasferendo, potremmo fornire un feedback più dettagliato su come affrontare il problema.

    
risposta data 29.12.2011 - 21:05
fonte
11

I feel like the only unit testing I could do would be to simply re-code the algorithm in the test

Sei quasi corretto per una classe così semplice.

Provalo per una calcolatrice più complessa .. Come un calcolatore per il punteggio di bowling.

Il valore dei test unitari è più facilmente visibile quando si hanno regole di "business" più complesse con diversi scenari da testare.

Non sto dicendo che non dovresti testare un run of the mill calcolatrice (i tuoi problemi con l'account della calcolatrice in valori come 1/3 che non possono essere rappresentati? Cosa fa con la divisione per zero?) ma tu vedrà il valore più chiaramente se testate qualcosa con più rami per ottenere copertura.

    
risposta data 29.12.2011 - 17:42
fonte
6

Nonostante il fanatismo religioso circa il 100% di copertura del codice, dirò che non tutti i metodi dovrebbero essere testati da unità. Solo funzionalità che contiene una logica aziendale significativa. Una funzione che aggiunge semplicemente il numero è inutile da testare.

I am currently coding mostly "procedural" functions that take ~10 booleans and a few ints and give me an int result based on this

C'è il tuo vero problema proprio lì. Se il test unitario sembra innaturalmente duro o inutile, è probabile a causa di un difetto di progettazione. Se fosse più orientato agli oggetti, le tue firme di metodo non sarebbero così massicce e ci sarebbero meno input da testare.

Non ho bisogno di entrare nel mio OO è superiore alla programmazione procedurale ...

    
risposta data 29.12.2011 - 17:48
fonte
3

Dal mio punto di vista i test unitari sono persino utili nella tua piccola classe di sommatori: non pensare di "ricodificare" l'algoritmo e pensarlo come una scatola nera con l'unica conoscenza di cui si ha a che fare è il comportamento funzionale (se si ha familiarità con la moltiplicazione veloce si conoscono alcuni tentativi più veloci, ma più complessi che usare il " * b ") e la tua interfaccia pubblica. Di quanto dovresti chiederti "Che diavolo potrebbe andare storto?" ...

Nella maggior parte dei casi succede al confine (vedo che testi già aggiungendo questi pattern ++, -, + -, 00 - tempo per completarli con - +, 0+, 0-, +0, - 0). Pensa a cosa succede a MAX_INT e MIN_INT quando aggiungi o sottrai (aggiungendo i negativi;)) lì. Oppure cerca di assicurarti che i tuoi test appaiano esattamente esattamente ciò che accade a circa lo zero.

Tutto sommato il segreto è molto semplice (forse anche per quelli più complessi;)) per classi semplici: pensa ai contratti della tua classe (vedi design per contratto) e poi prova contro di loro. Meglio sai che i tuoi inv, pre e post sono i "completatori" dei tuoi test.

Suggerimento per le classi di test: prova a scrivere solo una asserzione in un metodo. Dai ai metodi i buoni nomi (ad esempio "testAddingToMaxInt", "testAddingTwoNegatives") per ottenere il miglior feedback quando il test fallisce dopo la modifica del codice.

    
risposta data 29.12.2011 - 20:06
fonte
2

Invece di testare un valore di ritorno calcolato manualmente o duplicare la logica nel test per calcolare il valore di ritorno previsto, testare il valore di ritorno per una proprietà prevista.

Ad esempio, se vuoi testare un metodo che inverte una matrice, non vuoi invertire manualmente il valore di input, devi moltiplicare il valore di ritorno per l'input e controllare di ottenere la matrice di identità.

Per applicare questo approccio al tuo metodo, dovrai considerare il suo scopo e la sua semantica, per identificare quali proprietà il valore di ritorno avrà rispetto agli input.

    
risposta data 29.12.2011 - 19:34
fonte
2

I test unitari sono uno strumento di produttività. Riceverai una richiesta di modifica, la implementerai, quindi eseguirai il tuo codice attraverso l'unità di prova. Questo test automatizzato consente di risparmiare tempo.

I feel that this test is totally useless, because you are required to manually compute the wanted result and test it, I feel like a better unit test here would be

Un punto controverso. Il test nell'esempio mostra solo come istanziare una classe ed eseguirla attraverso una serie di test. Concentrandosi sui dettagli di una singola implementazione manca la foresta per gli alberi.

Can someone provide me with an example where unit testing is actually useful?

Hai un'entità Dipendente. L'entità contiene un nome e un indirizzo. Il cliente decide di aggiungere un campo ReportsTo.

void TestBusinessLayer()
{
   int employeeID = 1234
   Employee employee = Employee.GetEmployee(employeeID)
   BusinessLayer bl = new BusinessLayer()
   Assert.isTrue(bl.Add(employee))//assume Add returns true on pass
}

Questo è un test di base del BL per lavorare con un dipendente. Il codice passerà / fallirà la modifica dello schema che hai appena fatto. Ricorda che le asserzioni non sono l'unica cosa che fa il test. L'esecuzione del codice garantisce inoltre che non si verifichino eccezioni.

Nel tempo, avere i test in atto rende più semplice apportare modifiche in generale. Il codice viene automaticamente testato per le eccezioni e contro le asserzioni fatte. Ciò evita gran parte del sovraccarico sostenuto dai test manuali da parte di un gruppo QA. Mentre l'interfaccia utente è ancora piuttosto difficile da automatizzare, gli altri livelli sono in genere molto semplici se si utilizzano correttamente i modificatori di accesso.

I feel like the only unit testing I could do would be to simply re-code the algorithm in the test.

Anche la logica procedurale è facilmente incapsulata all'interno di una funzione. Incapsula, istanzia e passa nell'int / primitive da testare (o oggetto fittizio). Non copiare incollare il codice in un Test unitario. Che sconfigge ASCIUTTO. Inoltre, sconfigge completamente il test perché non si sta testando il codice, ma una copia del codice. Se il codice che avrebbe dovuto essere testato cambia, il test passa ancora!

    
risposta data 29.12.2011 - 20:02
fonte
2

Prendendo il tuo esempio (con un po 'di refactoring),

assert(a + b, math.add(a, b));

non aiuta a:

  • capire come math.add si comporta internamente,
  • sapere cosa succederà con i casi limite.

È più o meno come dire:

  • Se vuoi sapere che cosa fa il metodo, vai a vedere le centinaia di righe del codice sorgente (perché, sì, math.add può contenere centinaia di LOC; vedi sotto).
  • Non mi preoccupo di sapere se il metodo funziona correttamente. Va bene se entrambi i valori attesi e quelli effettivi sono diversi da ciò che mi aspettavo .

Questo significa anche che non devi aggiungere test come:

assert(3, math.add(1, 2));
assert(4, math.add(2, 2));

Non aiutano né, o almeno, una volta fatta la prima asserzione, la seconda non porta nulla di utile.

Invece, che dire:

const numeric Pi = 3.1415926535897932384626433832795;
const numeric Expected = 4.1415926535897932384626433832795;
assert(Expected, math.add(Pi, 1),
    "Adding an integer to a long numeric doesn't give a long numeric result.");
assert(Expected, math.add(1, Pi),
    "Adding a long numeric to an integer doesn't give a long numeric result.");

Questo è auto-esplicativo e dannatamente utile sia per te che per la persona che manterrà il codice sorgente in seguito. Immagina che questa persona apporti una leggera modifica a math.add per semplificare il codice e ottimizzare le prestazioni e vede il risultato del test come:

Test TestNumeric() failed on assertion 2, line 5: Adding a long numeric to an
integer doesn't give a long numeric result.

Expected value: 4.1415926535897932384626433832795
Actual value: 4

questa persona capirà immediatamente che il nuovo metodo modificato dipende dall'ordine degli argomenti: se il primo argomento è un numero intero e il secondo è un numero lungo, il risultato sarebbe un numero intero, mentre era previsto un valore numerico lungo .

Allo stesso modo, ottenere il valore effettivo di 4.141592 alla prima asserzione è autoesplicativo: sai che il metodo dovrebbe gestire una grande precisione , ma in realtà, fallisce.

Per lo stesso motivo, due asserzioni seguenti possono avere senso in alcune lingue:

// We don't expect a concatenation. 'math' library is not intended for this.
assert(0, math.add("Hello", "World"));

// We expect the method to convert every string as if it was a decimal.
assert(5, math.add("0x2F", 5));

Inoltre, che dire:

assert(numeric.Infinity, math.add(numeric.Infinity, 1));

Anche auto-esplicativo: vuoi che il tuo metodo sia in grado di gestire correttamente l'infinito. Passare oltre l'infinito o lanciare un'eccezione non è un comportamento previsto.

O forse, a seconda della lingua, questo avrà più senso?

/**
 * Ensures that when adding numbers which exceed the maximum value, the method
 * fails with OverflowException, instead of restarting at numeric.Minimum + 1.
 */
TestOverflow()
{
    UnitTest.ExpectException(ofType(OverflowException));

    numeric result = math.add(numeric.Maximum, 1));

    UnitTest.Fail("The tested code succeeded, while an OverflowException was
        expected.");
}
    
risposta data 30.12.2011 - 06:34
fonte
1

Per una funzione molto semplice come aggiungere, il test potrebbe essere considerato non necessario, ma man mano che le funzioni diventano più complesse, diventa sempre più ovvio il motivo per cui è necessario eseguire il test.

Pensa a ciò che fai quando esegui la programmazione (senza test dell'unità). Di solito scrivi del codice, eseguilo, vedi che funziona e passa alla prossima cosa, giusto? Mentre scrivi più codice, specialmente in un sistema / GUI / sito Web di grandi dimensioni, scopri che devi fare sempre di più "correre e vedere se funziona". Devi provare questo e provare quello. Quindi, apporti alcune modifiche e devi provare di nuovo le stesse cose. Diventa molto ovvio che potresti risparmiare tempo scrivendo unit test che automatizzano l'intera parte "in esecuzione e vedendo se funziona".

Man mano che i progetti diventano sempre più grandi, il numero di cose che devi "eseguire e vedere se funziona" diventa irrealistico. Quindi finisci semplicemente a correre e provare alcuni componenti principali della GUI / progetto e poi sperare che tutto il resto vada bene. Questa è una ricetta per il disastro. Ovviamente tu, come essere umano, non puoi più volte verificare tutte le possibili situazioni che potrebbero essere utilizzate dai tuoi clienti se la GUI è utilizzata da centinaia di persone. Se sono stati eseguiti test unitari, è sufficiente eseguire il test prima di spedire la versione stabile, o anche prima di eseguire il commit nel repository centrale (se il posto di lavoro ne utilizza uno). E, se ci sono bug rilevati in seguito, puoi semplicemente aggiungere un test unitario per verificarlo in futuro.

    
risposta data 29.12.2011 - 20:33
fonte
1

Uno dei vantaggi della scrittura di unit test è che ti aiuta a scrivere codice più robusto costringendoti a pensare ai casi limite. Che ne dici di testare alcuni casi limite, come l'overflow di numeri interi, il troncamento decimale o la gestione di valori null per i parametri?

    
risposta data 31.12.2011 - 19:34
fonte
0

Forse supponi che add () sia stato implementato con l'istruzione ADD. Se qualche programmatore o ingegnere hardware junior ha reimplementato la funzione add () utilizzando ANDS / ORS / XORS, i bit invertiti e i turni, è possibile testare l'unità con l'istruzione ADD.

In generale, se sostituisci il coraggio di add (), o l'unità in prova, con un numero casuale o un generatore di output, come sapresti che qualcosa è stato rotto? Codifica questa conoscenza nei tuoi test unitari. Se nessuno può dire se è rotto, basta controllare un codice per rand () e andare a casa, il lavoro è finito.

    
risposta data 30.12.2011 - 08:28
fonte
0

Potrei averlo perso tra tutte le risposte ma, per me, l'unità principale dietro Unit Testing è meno di dimostrare la correttezza di un metodo oggi ma che dimostra la continua correttezza di quello metodo quando [mai] lo cambia .

Prendi una semplice funzione, come restituire il numero di elementi in alcune raccolte. Oggi, quando la tua lista si basa su una struttura di dati interna che conosci bene, potresti pensare che questo metodo sia così dolorosamente ovvio che non hai bisogno di un test per questo. Quindi, in diversi mesi o anni, tu (o qualcun altro ) decidi [s] a sostituire la struttura interna dell'elenco. Devi ancora sapere che getCount () restituisce il valore corretto.

È qui che i tuoi Test delle unità sono davvero utili.

Puoi cambiare l'implementazione interna del tuo codice ma a qualsiasi utente di quel codice, il risultato rimane lo stesso.

    
risposta data 28.07.2015 - 13:44
fonte

Leggi altre domande sui tag