When, Where, and How to Unit Test [duplicato]

3

Ho molta familiarità con i framework xUnit e cerco di implementare i test unitari su ogni progetto che inizio. Da qualche parte lungo la strada, mi rendo conto che sto scrivendo gli stessi test più e più volte, e poi mi imbatto in un metodo davvero difficile da testare o un test che coinvolge risorse remote, e poi mi arrendo per un po '. Finisco per tornare con entusiasmo ogni volta che posso testare cose semplici, ma non appena mi imbatto in cose più difficili da testare, corro per le colline con la coda tra le gambe.

Facciamo alcuni esempi.

  1. Lungo flusso di cose da fare in un metodo. (Come faccio a testarlo? Mi limito ad assicurarmi che il codice venga chiamato eliminando le cose? Come dovrei riscrivere il codice per facilitare i test?)

    public void doSomething() {
        Object1 value1 = doSomething1(this.getName());
        if (value1.isError()) {
            sendError(new Object1Error(value1.getError().getMessage()));
            return;
        }
    
        Object2 value2 = doSomething2(value1, this.getName());
        if (value2.isError()) {
            sendError(new Object2Error(value2.getError().getMessage()));
            return;
        }
    
        /* ad infinitum... */
    }
    
  2. Metodi e costruttori sovraccaricati.

    public String toJSON() {
        return toJSON(true);
    }
    
    public String toJSON(boolean prettyPrint) {
        /* do work */
        return result;
    } 
    
    /* test */
    @Test public void testToJSON() { ... };
    
    @Test public void testToJSONBoolean() { /* redundant similar test code... */ }
    
  3. Risorse remote. Come faccio a testare che la mia API di caricamento funzioni (es. Metodo di richiesta giusta, host, tipo di contenuto della richiesta, payload, ecc.) Senza fare effettivamente un upload? Questi caricamenti vengono inviati a server al di fuori del mio controllo.

    public void doUpload() throws IOException {
        HttpClient client = new HttpClient();
        PutMethod putMethod = new PutMethod(...);
        putMethod.setRequestHeader(...);
        putMethod.setRequestEntity(new FileInputStreamRequestEntity(...));
    
        /* etc. */
    
        final int responseCode = client.executeMethod(putMethod);
    }
    
  4. Verifica interazioni server con i clienti.

    /* before we even get here, other interactions need to take place */
    public void updateClientProfile(UserProfile profile) {
        // 1. validate the input
        // 2. update the user in the db
        // 3. generate an email in HTML
        // 4. send the email
        // 5. serve out a view
    }
    

I test a prima vista sembrano facili, finché non arrivo a questo tipo di situazioni. Come mantenere la mia motivazione per i test e scrivere il mio codice per essere più testabile?

Sembra che più cose che un metodo deve fare, i test unitari esponenzialmente più difficili e frustranti da scrivere. Devo testare semplicemente che il codice sta chiamando i giusti servizi e metodi? Scrivo test unitari semplicemente per provare a me stesso che il mio codice chiama i metodi che ho detto di chiamare o per determinare se la logica è giusta? Aiuto!

    
posta Naftuli Kay 18.07.2012 - 03:09
fonte

4 risposte

6

"Non incolpare lo specchio per la brutta faccia"

I problemi che osservi nei test riflettono onestamente i problemi con la qualità del codice sorgente.

È molto ( molto ) improbabile che continuare a testare unitamente i test unitari sul codice che hai trovato mal progettato possa aiutarti a fare ulteriori progressi qui. Ci ho provato e ho fallito. Sono stato pronto a convincere me stesso che è colpa mia, che sono solo io incapace di farlo funzionare, ma ho lasciato cadere quell'idea dopo aver visto che colleghi molto più talentuosi di me stanno avendo esattamente gli stessi problemi (un vantaggio di lavorare in modo strong il team è che puoi valutare facilmente quando le cose vanno male a causa dei tuoi limiti e quando accade perché le cose sono sbagliate).

La tua migliore scommessa sarebbe quella di entrare in contatto con i tester professionisti, impostare un accurato controllo di qualità per ottenere regressioni funzionali e dopo che, refactoring del codice in qualcosa di meglio - in qualcosa che renda produttivi i test di unità di scrittura e divertente, come dovrebbe essere.

Citando self - la cosa è, gli approcci applicabili con codebase migliori non lo fanno taglia:

With bad code... unit tests do things opposite to how I use them in good code, like breaking at reasonable changes and failing to catch the real mistakes I make. Which is painful but not surprising - what else would one expect from testing units which have bad design to start with? Instead of helping you improve the design, unit tests often work to preserve bad code - eg in my recent maintenance project I was regularly removing large chunks of code which was only referenced from outdated senseless unit tests. BTW I use that knowledge when writing new code: when I find out that my unit tests tend to get too complicated / fragile, this indicates a need to fix some issue with my own design...

    
risposta data 18.07.2012 - 04:38
fonte
4

Per prima cosa, Quando il test di unità è inappropriato o Non necessario?

Questo è comunque abbastanza diverso.

I end up coming back eagerly whenever I can to test simple things, but as soon as I run into harder things to test, I run for the hills with my tail between my legs.

Questo è il problema più grande. Se non ottieni nient'altro dalla risposta: concentrati su questo. Questo è un problema che avrà un impatto sulla tua programmazione, sulla tua carriera ... su tutto. Il corso si farà duro e imparare a gestirlo in modo efficace è un'abilità personale fondamentale.

Sugli esempi:

1 - a method doing a series of operations

Verifica le singole operazioni separatamente. Se possono essere derisi, deriderli aiuterà a testare questo metodo. Rompendo questo metodo nel fare meno cose sarà d'aiuto. Se appropriato, avere un costrutto che esegue una sequenza di delegati / oggetti funzione nell'ordine a meno che non si verifichi un errore aiuterà perché in tal modo è possibile testare tale costrutto con funzioni arbitrarie che si comportano meglio. Al rialzo, quel costrutto può essere riutilizzato se ti imbatti in uno schema simile in futuro.

Per questo particolare esempio, prenderei in considerazione il refactoring per usare le eccezioni piuttosto che i codici di errore. Dipende da cosa sta facendo il codice reale e dal supporto delle eccezioni della lingua / piattaforma.

2 - a method overload that trivially calls another

Personalmente non testerei il sovraccarico banale, solo la funzione principale. Il sovraccarico banale è ovviamente corretto, e se cambia abbastanza da rompere le cose, gli altri test dovrebbero fallire quando il true implicito non è corretto.

3 - methods using external resources

Ci sono tre cose da considerare qui, poiché non è chiaro che cosa implichi l'esempio:

  1. Se puoi, prendi in giro le risorse esterne. Finché il tuo codice invia le cose giuste all'API "conosciute" (e reagisce correttamente alla risposta prevista), allora è abbastanza buono.
  2. Assicurati di non testare l'API "noto"
  3. Se è abbastanza importante, allora vale la pena fare test automatici che non sono unit test. Avere un server che può controllare e testare se il caricamento funziona. Questi sono test di integrazione o test di sistema o semplicemente test funzionali automatizzati a seconda del loro ambito e di chi stai parlando. Questi sono ancora utili in scenari come questi in cui è necessario testare qualcosa, ma non possono efficacemente testarli unitamente.

4 - server processing a request

Questo è simile ad alcuni dei consigli precedenti: scoppia i componenti, prova quelli individualmente, prendi in giro dove possibile, usa i test di integrazione, se appropriato.

Una nota sul mocking

Questo è un punto di cui ho parlato un paio di volte qui, ma potrebbe non essere chiaro cosa intendo o come può aiutarti. Beffardo.

Mocking è l'uso di un oggetto specifico del test (un oggetto fittizio) al posto di un oggetto reale per isolare il codice che stai cercando di testare. Esistono numerosi framework per automatizzare questo processo. Personalmente, trovo che questi siano spesso fragili e eccessivamente complessi. A volte sono fondamentali per ottenere cose che non puoi raggiungere. L'oggetto simulato più semplice è quello che tu fai e fornisci.

Durante la progettazione del test, ogni volta che chiami qualcosa per ottenere dati o inviare dati all'esterno, considera un oggetto fittizio. Ti consentono di fornire i dati corretti per il test. Ti permettono di verificare che i dati rilegati verso l'esterno siano ciò che dovrebbero essere.

Tuttavia sono un po 'overhead e possono rendere la manutenzione del test più onerosa. Se ti senti di averne bisogno molto, può essere un segnale che il tuo codice è troppo accoppiato. Basta essere consapevoli di queste cose e pesarle contro il rialzo quando si esegue il test design. Per cose come l'accesso ai database, sono inestimabili.

    
risposta data 18.07.2012 - 04:31
fonte
3

Mi sembra che tu stia tentando di scegliere i casi di test in base ai metodi che hai già scritto. Questo sta mettendo un po 'il carro davanti al cavallo; tende a condurti verso la scrittura di test su ciò che tu già sai che la classe fa, invece di testare ciò che è necessario fare. Se il comportamento richiesto non è stato implementato correttamente (o del tutto), probabilmente il test dell'unità non lo troverà per te.

Quindi, piuttosto che scrivere unit test in base ai metodi che hai implementato, metti da parte il tuo codice sorgente per un momento. Invece, scrivi casi di test in base al comportamento che la classe deve avere. Torna ai requisiti documentati, se li hai, e considera i diversi scenari che possono riguardare ciascun requisito. Ogni combinazione di un requisito e uno scenario fornirà quindi un metodo xUnit. E dai i nomi dei metodi xUnit che riflettono il comportamento previsto della tua classe, non il nome del metodo che implementa quel comportamento: in altre parole, un nome del metodo di prova come calculatedPriceEqualsQuantityTimesUnitPrice è molto meglio di testCalculatePrice .

In termini di problemi specifici sopra -

  • Il tuo metodo doSomething potrebbe probabilmente fare con un metodo di prova per ogni possibile comportamento; cioè, per ogni percorso attraverso il metodo.
  • I requisiti documentati del tuo metodo toJSON(Boolean) ti diranno quanti metodi di test hai bisogno e quale scenario dovrebbero coprire ciascuno. Va bene avere un metodo comune nella classe di test, che molti di questi metodi di test chiamerebbero. Probabilmente non c'è bisogno di testare toJSON() separatamente - la sua logica è davvero troppo semplice per andare male.
  • Per le risorse remote e per le interazioni server, il trucco è lo stesso. Raggruppa il codice che interagisce con la risorsa (o il server) in una singola classe, senza logica propria (una "classe wrapper"). La classe wrapper passerà semplicemente le richieste direttamente alla risorsa o al server in questione e restituirà la risposta fornita dalla risorsa o dal server. Quando collaudi unitamente la classe che UTILIZZA la risorsa, devi fornire una simulazione della classe wrapper (ovvero, un oggetto fittizio il cui comportamento puoi controllare direttamente e le cui interazioni vengono registrate in modo che tu possa verificarle in seguito). Poiché la classe wrapper non ha una logica propria, non c'è nulla da testare. Tuttavia, ciò che puoi fare è scrivere un "test di integrazione di basso livello", che è simile a un test di unità, ma include la risorsa che la classe wrapper sta eseguendo il wrapping. Di solito puoi scrivere questi in xUnit. Lo scopo di tale test è garantire che si interagisca correttamente con la risorsa o il server; ma di solito puoi mantenere i casi di test molto semplici, perché ogni metodo della classe wrapper dovrebbe essere molto semplice.
risposta data 18.07.2012 - 10:46
fonte
1

Ogni volta che colpisci un metodo difficile da testare, è molto probabile che il metodo faccia troppo. Guarda questo esempio che hai dato:

public void updateClientProfile(UserProfile profile) {
    // 1. validate the input
    // 2. update the user in the db
    // 3. generate an email in HTML
    // 4. send the email
    // 5. serve out a view
}

Questo fa molto di più che semplicemente aggiornare il profilo del cliente. Ciò che in realtà dovrebbe essere chiamato questo metodo è validateProfileAndUpdateIfValidThenEmailUserAndServeView (). Questo dovrebbe essere il tuo suggerimento che il metodo sta facendo troppo.

Ora puoi avere questo metodo, ma devi astrarre i diversi componenti.

private ValidationService validator; // inject these however you choose
private EmailerService emailer;
private UserProfileRepository users;

public UserClientView processClientUpdateRequest(UserProfile profile) {
    if (!validator.validateUser(profile)) {
        return new FailureUserClientView(profile);
    }

    users.add(profile);
    emailer.notifyUserOfProfileChange();

    return new SuccessUserClientView(profile);
}

Scopri quanto è più facile provare ora?

Puoi, ad esempio, eliminare il tuo repository e il tuo emailer con oggetti che non fanno nulla, prendere in giro il validatore puoi testare tutti questi metodi separatamente) e semplicemente testare la logica di validazione (cioè il tipo restituito dal metodo è in base alla risposta di validateUser).

Ecco due test.

Aggiungi altri due per verificare che, se il validatore risponde positivamente, il DB viene chiamato e l'emailer viene notificato e il metodo viene utilizzato in gran parte. Hai ancora l'e-mail e i servizi di validazione da testare (il repository dovrebbe essere un livello molto sottile per il database, quindi non dovrebbe beneficiare dei test delle unità), ma ognuno di questi è molto più semplice da gestire rispetto al metodo che hai fornito tu. / p>

Questo è l'intero principio alla base di TDD: scrivi prima i test e calcoli questa cosa molto prima, prima di iniziare a sviluppare il metodo stesso. Vedete molto rapidamente che il nome del metodo non è descrittivo e se lo rendete descrittivo indica un metodo che fa troppo.

Nota, l'unico problema che descrivi che non è risolto dai servizi di simulazione e di stub è il metodo sovraccarico.

Ora questo è un problema molto diverso. Questo è quando il test diventa dogmatico e diventa più di un ostacolo che un beneficio. Dai il tuo esempio:

public String toJSON() {
    return toJSON(true);
}

public String toJSON(boolean prettyPrint) {
    /* do work */
    return result;
}

Finché il secondo metodo è testato, stai bene. Scrivere test per il metodo esterno non cambierà affatto il tuo design, né evidenzierà un bug di regressione in seguito. Quindi non ha alcuno scopo.

    
risposta data 18.07.2012 - 12:38
fonte