Aiuta a garantire che i test unitari siano significativi

3

Ho appena scritto un test unitario per questa funzione, che scorre attraverso una raccolta di date e imposta proprietà uguali a true o false a seconda che siano prima o dopo una determinata data di confronto:

public void CheckHistory(int months)
{
    var endDate = DateTime.Today.AddMonths(months);
    Dictionary<Order, bool> orders = new Dictionary<Order, bool>();

    foreach (var kvp in this.Orders)
    {
        if (kvp.Key.Date >= endDate)
        {
            orders.Add(kvp.Key, true);
        }
        else
        {
            orders.Add(kvp.Key, false);
        }
    }
    this.OrderHistories = orders;
}

Quindi ecco il test che ho scritto:

public void Assert_CheckHistory_SelectsCorrectDates()
{
    MyViewModel vm = GetVmWithMockRepository();
    vm.OrderHistories = new Dictionary<OrderHistory, bool>();

    OrderHistory ohOld = new OrderHistory();
    ohOld.MailingDate = DateTime.Today.AddMonths(-12);
    vm.OrderHistories.Add(ohOld, false);

    OrderHistory ohNew = new OrderHistory();
    ohNew.MailingDate = DateTime.Today.AddMonths(-3);
    vm.OrderHistories.Add(ohNew, false);

    vm.CheckOrderHist(-6);

    int selectedOrders = vm.OrderHistories.Where(o => o.Value == true).Count();
    Assert.AreEqual(1, selectedOrders, "Unexpected number of selected Order Histories");
}

Niente di sbagliato lì. Il test passa e tutto va bene con il mondo.

Tuttavia, sono ossessionato dal fastidioso sentimento che in realtà non sto testando nulla di utile, e sto solo scrivendo test per il bene.

Ho questo lotto . Una paranoia strisciante che i test che sto scrivendo sono incompleti nel senso che mentre coprono le linee di codice nella funzione obiettivo, non catturano realmente i probabili problemi e sono quindi solo un sovraccarico di manutenzione.

Vale la pena provare questo test campione? È persino un test mal progettato che vale la pena di non superare nessun test? E soprattutto ci sono dei principi per aiutare i programmatori a identificare se un test è utile o meno, o per guidarli nella costruzione di test utili in futuro?

Per essere chiari, sto aggiungendo test a un'applicazione esistente. Non è possibile eseguire test-first in vero stile TDD.

    
posta Matt Thrower 07.05.2014 - 15:44
fonte

4 risposte

7

Per citare il "Way of Testivus" ,

A bad test is better than no test

Assicurati che il tuo test ti dica che il codice funziona è il punto del test. Quando scrivi dei test per il codice che è già stato scritto, stai impostando le cose in modo che tu possa refactoring ed essere sicuro di non aver infranto alcuna funzionalità corrente. Se il test fallisce senza un motivo apparente, il test è negativo e dovrebbe essere corretto. Finché il test garantisce la funzionalità dell'applicazione, il test ha un certo valore.

La scrittura di questi test ti dà fiducia nella tua capacità di refactoring di quel codice in seguito. Perché se hai fatto bene, il test passerà ancora. Ti stai dando la sicurezza di cambiare quel codice e non devi preoccuparti di rompere qualsiasi vecchia funzionalità.

Uncle Bob ha avuto un recente post sul blog in merito a questo:

If you have a test suite that you trust so much that you are willing to deploy the system based solely on those tests passing; and if that test suite can be executed in seconds, or minutes, then you can quickly and easily clean the code without fear.

Il tuo test di esempio non è poi così buono ma ti dà più fiducia che qualsiasi modifica apportata non influenzi la funzionalità. Si desidera verificare che per un determinato stato il metodo restituisca una risposta specifica. Stai provando a testare cosa fa il codice piuttosto che quale sia il codice. Per un codice legacy come questo, ti ritroverai con test più "funzionali" piuttosto che "unitari".

Questo test ti dà maggiore sicurezza sul fatto che eventuali modifiche all'applicazione non abbiano causato la rottura del codice? In caso contrario, il test non è utile e dovresti eliminarlo o cambiarlo. Il codice non testato paralizzerà tutte le modifiche utili che puoi apportare in seguito.

"Non possiamo apportare quel cambiamento perché non sappiamo ovunque che venga usato!" I tuoi test rendono questo argomento discutibile. Sai che il codice funziona tutto perché tutti i test passano.

I test inoltre non garantiscono un codice privo di bug. Il tuo codice potrebbe avere ancora dei bug, ma quando vengono scoperti e aggiungi un nuovo test duplicandolo. Assicurati che quel bug non accada MAI più perché poi quel test fallirà. E fintanto che esegui spesso i tuoi test, questo bug verrà sempre controllato.

    
risposta data 07.05.2014 - 16:13
fonte
5

Quindi ... Lasciatemi suggerire un refactoring:

public void CheckHistory(int months)
{
    var endDate = DateTime.Today.AddMonths(months);
    Dictionary<Order, bool> orders = new Dictionary<Order, bool>();

    foreach (var kvp in this.Orders)
    {
        orders.Add(kvp.Key, kvp.Key.Date < endDate);
    }
    this.OrderHistories = orders;
}

Bello, giusto? Beh, forse carino, ma non giusto. Perché io (fingiamo inavvertitamente) ho capovolto la direzione dell'operatore di disuguaglianza. Il tuo test l'avrebbe scoperto? In realtà (credo) no, perché il numero prima di & dopo è lo stesso, e stai solo testando che il conteggio è 1. Quindi probabilmente dovresti pensare a migliorarlo un po '. Ma in termini di valore di avere il test unitario, di avere quel tipo di copertura del test: Sì, è prezioso, perché ti consente di refactoring in modo sicuro.

    
risposta data 07.05.2014 - 16:13
fonte
2

Il valore di un test di regressione non sta avendo successo quando le cose vanno bene - potresti ottenere lo stesso effetto dipingendo un rettangolo verde sul tuo monitor! Il suo valore è in non riuscire quando le cose sono sbagliate , e farlo il prima possibile, prima che il costo di correggere il problema diventi troppo grande.

Il modo più semplice per assicurarsi che fallisca quando le cose sono sbagliate è scriverlo per primo e verificare che non funzioni. Scegli qualsiasi situazione che ti capita che possa andare storta con i calcoli della data, scrivi un test che controlli questo, eseguilo su un'implementazione stub e guardalo fallire. Quindi puoi scrivere il codice commerciale che gestisce questo caso e vederlo avere successo. Questo ti dà due garanzie vitali:

  1. La tua logica aziendale è corretta
  2. Se, in futuro, qualcuno cambierà il codice aziendale in modo che gestisca questo caso in modo errato, lo saprai immediatamente .

Coding test-first non è l'unico modo per garantire che i tuoi test siano appropriati e affidabili - è solo il modo più semplice e semplice che riesco a pensare. Molte persone sono inizialmente escluse dai passi apparentemente fuori ordine, ma se si considera cosa si suppone debba fare un test, lo trovo piuttosto ovvio.

    
risposta data 07.05.2014 - 15:51
fonte
1

Vedo che ti imbatti in problemi comuni quando esegui test.

Un mio insegnante scherzava spesso su ciò che di solito accade in laboratorio per l'ingegneria del software: uno studente crea 3-4 casi di test e se non rompono l'applicazione era tutto a posto ed erano pronti per il prossimo incarico.

Un malinteso comune sui test è che si suppone che copra i casi di test che non infrangono il software. In realtà è l'esatto contrario: con i test si tenta di interrompere la tua applicazione e vedere cosa succede.

Un altro problema è il modo in cui i casi di test sono definiti e il loro numero. Per 5 casi che passa la tua applicazione ce ne sono forse 10000000 che la infrangono. Ovviamente, poiché abbiamo un tempo limitato da dedicare ai test, non è necessario e in molti casi è impossibile coprire tutti i casi di test per un determinato problema (specialmente quelli più complessi).

Ecco perché sono stati creati strumenti come QuickCheck . Con QuickCheck (disponibile non solo per Haskell, per cui è stato originariamente creato, ma anche per molti altri linguaggi) si definisce la logica che una determinata funzione deve soddisfare e quindi si sono generati tutti i casi di test che si desidera. Questo è come dovrebbero essere fatti i test.

Dal tuo codice presumo che stai usando C #. Non c'è QuickCheck per C # (purtroppo) ma hai concetto di invarianti e contratti più Pex , che migliora molto l'intera esperienza di testing dell'unità.

    
risposta data 07.05.2014 - 19:17
fonte

Leggi altre domande sui tag