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 effettivo. 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, poiché è necessario calcolare manualmente il risultato desiderato e testarlo, penso che un test unitario migliore qui sarebbe

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

ma allora questo sta semplicemente codificando la funzione stessa nel test. Qualcuno può fornirmi un esempio in cui il test unitario è effettivamente utile? Cordiali saluti, attualmente sto codificando principalmente funzioni "procedurali" che prendono ~ 10 booleani e alcuni ints e mi danno un risultato int basato su questo, penso che l'unico test unitario che potrei fare sarebbe semplicemente ricodificare l'algoritmo nel test. modifica: avrei anche dovuto precisare che è durante il porting (possibilmente mal progettato) codice ruby ​​(che non ho fatto)


14
How does unit testing work?Nessuno lo sa davvero :)
yannis,

30
"ti viene richiesto di calcolare manualmente il risultato desiderato". Come è "totalmente inutile"? In quale altro modo puoi essere sicuro che la risposta sia giusta?
S.Lott

9
@ S.Lott: Si chiama progresso, nell'antichità le persone usavano i computer per sgranocchiare i numeri e risparmiare tempo, nei giorni nostri le persone passano il tempo per assicurarsi che i computer possano sgranocchiare i numeri: D
Coder

2
@Coder: lo scopo del test unitario non è "scricchiolare i numeri e risparmiare tempo";)
Andres F.

7
@lezebulon: l'esempio di Wikipedia non è molto buono, ma questo è un problema con quel particolare caso di test, non con i test unitari in generale. Circa la metà dei dati del test dell'esempio non aggiunge nulla di nuovo, il che lo rende ridondante (temo di pensare a cosa farebbe l'autore di quel test con scenari più complessi). Un test più significativo partizionerebbe i dati del test almeno nei seguenti scenari: "può aggiungere numeri negativi?", "È zero neutro?", "Può aggiungere un numero negativo e un numero positivo?".
Andres F.

Risposte:


26

I test unitari, se stai testando unità abbastanza piccole, affermano sempre ciò che è palesemente ovvio.

Il motivo per cui add(x, y)viene menzionato persino un unit test, è perché qualche tempo dopo qualcuno entrerà adde inserirà un codice di gestione della logica fiscale speciale senza rendersi conto che l'aggiunta viene utilizzata ovunque.

I test unitari riguardano molto il 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 sembra un metodo fantastico per testare l'unità, perché ha uno scopo molto chiaro. Tuttavia, fa circa 5 cose diverse. Ogni cosa ha un caso valido e non valido e farà un'enorme permutazione dei test unitari. Idealmente, questo sarebbe 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 alle unità. A un test unitario non interessa ciò che _userRepoconsidera un comportamento valido FetchValidUser, solo che viene chiamato. È possibile utilizzare un altro test per garantire esattamente ciò che costituisce un utente valido. Allo stesso modo per CheckUserForRole... hai disaccoppiato il tuo test dal sapere che aspetto ha la struttura del ruolo. Hai anche disaccoppiato l'intero programma da essere strettamente legato Session. Immagino che tutti i pezzi mancanti qui sarebbero:

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;
    }
}

Rifattorando hai realizzato diverse cose contemporaneamente. Il programma è molto più utile per strappare le strutture sottostanti (puoi abbandonare il livello del database per NoSQL) o aggiungere senza problemi il blocco una volta che ti rendi conto che Sessionnon è thread-safe o altro. Ora ti sei anche dato dei test molto diretti per scrivere per queste tre dipendenze.

Spero che sia di aiuto :)


13

Attualmente sto codificando principalmente funzioni "procedurali" che prendono ~ 10 booleani e alcuni ints e mi danno un risultato int basato su questo, penso che l'unico test unitario che potrei fare sarebbe semplicemente ricodificare l'algoritmo nel test

Sono abbastanza sicuro che ciascuna delle tue funzioni procedurali sia deterministica, quindi restituisce un risultato int specifico per ogni dato 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 insiemi di valori di input e registrare i risultati. Quindi, è necessario CODICE DURO i risultati nel test. Il test dovrebbe essere una prova del fatto che il tuo codice produce effettivamente risultati che sono noti per essere corretti .


+1 per l'esecuzione del codice esistente e la registrazione dei risultati. In questa situazione, questo è probabilmente l'approccio pragmatico.
MarkJ

12

Dal momento che 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 )
    }

Tu dici:

Ritengo che questo test sia totalmente inutile, perché è necessario calcolare manualmente il risultato desiderato e testarlo

Ma il punto è che è molto meno probabile che tu commetta un errore (o almeno più probabilità di notare i tuoi errori) quando esegui i calcoli manuali rispetto alla codifica.

Con quale probabilità commetti un errore nel tuo codice di conversione da decimale a romano? Abbastanza probabile Con quale probabilità commetti un errore quando converti manualmente i numeri decimali in numeri romani? Non molto probabilmente Ecco perché testiamo contro i calcoli manuali.

Con quale probabilità commetti un errore durante l'implementazione di una funzione radice quadrata? Abbastanza probabile Con quale probabilità commetti un errore nel calcolo manuale di una radice quadrata? Probabilmente più probabile. Ma con sqrt, puoi usare una calcolatrice per ottenere le risposte.

Cordiali saluti, attualmente sto codificando principalmente funzioni "procedurali" che prendono ~ 10 booleani e alcuni ints e mi danno un risultato int basato su questo, penso che l'unico test unitario che potrei fare sarebbe semplicemente ricodificare l'algoritmo nel test

Quindi speculerò su ciò che sta accadendo 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 gli output corretti. Ma devi testare quegli output con qualcosa che si sa sia corretto. Non serve scrivere il proprio algoritmo per calcolarlo perché potrebbe non essere corretto. In questo caso è troppo difficile calcolare manualmente i valori.

Ritornerei al codice ruby ​​ed eseguirò queste funzioni originali con vari parametri. Prenderei i risultati del codice ruby ​​e li inserirei nel test unitario. In questo modo non è necessario eseguire il calcolo manuale. Ma stai testando contro il codice originale. Ciò dovrebbe aiutare a mantenere i risultati uguali, ma se ci sono bug nell'originale, allora non ti aiuterà. Fondamentalmente, è possibile trattare il codice originale come la calcolatrice nell'esempio sqrt.

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


E se il codice Ruby ha un bug di cui non sai che non è nel tuo nuovo codice e il tuo codice fallisce un test unitario basato sugli output di Ruby, allora l'indagine sul perché fallito alla fine ti confermerà e si tradurrà in bug latente di Ruby trovato. Quindi è abbastanza bello.
Adam Wuerl,

11

Sento che l'unico test unitario che potrei fare sarebbe semplicemente ricodificare l'algoritmo nel test

Hai quasi ragione per una classe così semplice.

Provalo per una calcolatrice più complessa .. Come una calcolatrice per il punteggio del 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 una corsa del calcolatore del mulino (Il tuo calcolatore conta problemi con valori come 1/3 che non possono essere rappresentati? Che cosa fa con la divisione per zero?) Ma vedrai il valutare più chiaramente se si verifica qualcosa con più rami per ottenere copertura.


4
+1 per notare che diventa più utile per funzioni complicate. E se decidessi di estendere adder.add () ai valori in virgola mobile? Matrici? Valori dell'account leger?
joshin4colours,

6

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

Attualmente sto codificando principalmente funzioni "procedurali" che prendono ~ 10 booleani e alcuni ints e mi danno un risultato int basato su questo

C'è il tuo vero problema proprio lì. Se i test unitari sembrano innaturalmente difficili o inutili, è probabilmente a causa di un difetto di progettazione. Se fosse più orientato agli oggetti, le firme del metodo non sarebbero così enormi e ci sarebbero meno input possibili da testare.

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


in questo caso la "firma" del metodo non è massiccia, ho appena letto da uno std :: vector <bool> che è un membro della classe. Avrei anche dovuto precedere che sto eseguendo il porting (forse mal progettato) del codice ruby ​​(che non ho fatto)
lezebulon

2
@lezebulon Indipendentemente se ci sono così tanti input possibili da accettare per quel singolo metodo, quel metodo sta facendo troppo .
maple_shaft

3

Dal mio punto di vista, i test unitari sono utili anche per la tua piccola classe di sommatori: non pensare a "ricodificare" l'algoritmo e pensalo come una scatola nera con l'unica conoscenza che hai sul comportamento funzionale (se hai familiarità con la moltiplicazione rapida conosci alcuni tentativi più veloci, ma più complessi dell'uso di "a * b") e della tua interfaccia pubblica. Di quanto dovresti chiederti "Che diavolo potrebbe andare storto?" ...

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

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

Suggerimento per le classi di test: prova a scrivere una sola asserzione in un metodo. Assegna ai metodi nomi validi (ad es. "TestAddingToMaxInt", "testAddingTwoNegatives") per avere il miglior feedback quando il test fallisce dopo la modifica del codice.


2

Anziché testare un valore restituito calcolato manualmente o duplicare la logica nel test per calcolare il valore restituito previsto, testare il valore restituito per una proprietà prevista.

Ad esempio, se si desidera testare un metodo che inverte una matrice, non si desidera invertire manualmente il valore di input, è necessario moltiplicare il valore di ritorno per l'input e verificare di ottenere la matrice di identità.

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


2

I test unitari sono uno strumento di produttività. Si riceve una richiesta di modifica, la si implementa, quindi si esegue il codice attraverso il gambit di unit test. 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 creare un'istanza di una classe ed eseguirla attraverso una serie di test. Concentrandosi sulla minuzia 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 client 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 appena effettuata. Ricorda che le affermazioni non sono l'unica cosa che fa il test. L'esecuzione del codice garantisce inoltre che non vengano formulate eccezioni.

Nel tempo, avere i test in atto rende più semplice apportare modifiche in generale. Il codice viene testato automaticamente per le eccezioni e contro le asserzioni fatte. Ciò evita gran parte delle spese generali sostenute dai test manuali da parte di un gruppo di controllo qualità. Mentre l'interfaccia utente è ancora piuttosto difficile da automatizzare, gli altri livelli sono generalmente molto facili supponendo che tu usi 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, crea un'istanza e passa l'int / primitive da testare (o oggetto simulato). Non copiare incollare il codice in un Unit Test. Questo 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 continua comunque!


<pedantry> "gamut", non "gambit". </pedantry>
cHao

@chao lol impara qualcosa di nuovo ogni giorno.
P.Brian.Mackey,

2

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

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

non aiuta a:

  • capire come math.addsi comporta internamente,
  • sapere cosa accadrà con casi limite.

È quasi come dire:

  • Se vuoi sapere cosa fa il metodo, vai a vedere tu stesso le centinaia di righe di 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 sia i valori attesi che quelli effettivi sono diversi da quelli che mi aspettavo davvero .

Questo significa anche che non è necessario aggiungere test come:

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

Non aiutano nessuno dei due, o almeno, una volta che hai fatto la prima affermazione, la seconda non porta nulla di utile.

Invece, che dire di:

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 è autoesplicativo e dannatamente utile sia per te che per la persona che manterrà il codice sorgente in seguito.Immagina che questa persona esegua una leggera modifica per math.addsemplificare il codice e ottimizzare le prestazioni e vedere i risultati 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 metodo appena 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 numero lungo.

Allo stesso modo, ottenere il valore effettivo 4.141592alla prima asserzione è autoesplicativo: sai che il metodo dovrebbe affrontare 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 di:

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

Anche autoesplicativo: vuoi che il tuo metodo sia in grado di affrontare correttamente l'infinito. Andando 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.");
}

1

Per una funzione molto semplice come aggiungi, il test potrebbe essere considerato superfluo, ma poiché le funzioni diventano più complesse, diventa sempre più ovvio il motivo per cui il test è necessario.

Pensa a cosa fai quando stai programmando (senza test unitari). Di solito scrivi un po 'di codice, lo esegui, vedi che funziona e vai alla prossima cosa, giusto? Mentre scrivi più codice, specialmente in un sistema / GUI / sito Web molto grande, scopri che devi fare sempre di più "correre e vedere se funziona". Devi provare questo e provarlo. Quindi, fai alcune modifiche e devi provare di nuovo le stesse cose. Diventa molto ovvio che potresti risparmiare tempo scrivendo unit test che automatizzerebbero l'intera parte "correre e vedere se funziona".

Man mano che i tuoi progetti diventano sempre più numerosi, il numero di cose che devi "eseguire e vedere se funziona" diventa irrealistico. Quindi finisci per 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 testare ripetutamente ogni singola possibile situazione che i tuoi clienti potrebbero usare se la GUI fosse usata letteralmente da centinaia di persone. Se disponevi di test unitari, puoi semplicemente eseguire il test prima di spedire la versione stabile o anche prima di eseguire il commit nel repository centrale (se il tuo posto di lavoro ne utilizza uno). E, se ci sono dei bug trovati in seguito, puoi semplicemente aggiungere un test unitario per verificarlo in futuro.


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 overflow di numeri interi, troncamento decimale o gestione di valori null per i parametri?


0

Forse supponi che add () sia stato implementato con l'istruzione ADD. Se un programmatore junior o un ingegnere hardware hanno reimplementato la funzione add () utilizzando ANDS / ORS / XORS, invertitori di bit e spostamenti, potresti voler testare l'unità rispetto all'istruzione ADD.

In generale, se si sostituisce il budello di add () o l'unità sotto test, con un numero casuale o un generatore di output, come si fa a sapere che qualcosa è rotto? Codifica questa conoscenza nei tuoi test unitari. Se nessuno è in grado di dire se è rotto, allora controlla un po 'di codice per rand () e vai a casa, il tuo lavoro è finito.


0

Potrei essermelo sfuggito tra tutte le risposte ma, per me, l'unità principale dietro Unit Testing è meno quella di dimostrare la correttezza di un metodo oggi, ma che dimostra la continua correttezza di quel metodo quando [mai] lo cambi .

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

È qui che i test delle tue unità entrano davvero in gioco.

Puoi modificare l'implementazione interna del tuo codice ma per tutti gli utenti di quel codice, il risultato rimane lo stesso.

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.