Come testare un'unità di una funzione che è refactored al modello di strategia?

10

Se ho una funzione nel mio codice che va come:

class Employee{

    public string calculateTax(string name, int salary)
    {
        switch (name)
        {
            case "Chris":
                doSomething($salary);
            case "David":
                doSomethingDifferent($salary);
            case "Scott":
               doOtherThing($salary);               
       }
}

Normalmente mi rifatterò per usare il ploymorphism usando un modello di classe e strategia di fabbrica:

public string calculateTax(string name)
{
    InameHandler nameHandler = NameHandlerFactory::getHandler(name);
    nameHandler->calculateTax($salary);
}

Ora, se stavo usando TDD, avrei alcuni test che funzionano sull'originale calculateTax() prima del refactoring.

ex:

calculateTax_givenChrisSalaryBelowThreshold_Expect111(){}    
calculateTax_givenChrisSalaryAboveThreshold_Expect111(){}

calculateTax_givenDavidSalaryBelowThreshold_Expect222(){}   
calculateTax_givenDavidSalaryAboveThreshold_Expect222(){} 

calculateTax_givenScottSalaryBelowThreshold_Expect333(){}
calculateTax_givenScottSalaryAboveThreshold_Expect333(){}

Dopo il refactoring avrò una classe Factory NameHandlerFactory e almeno 3 implementazione di InameHandler .

Come devo procedere al refactoring dei miei test? Devo eliminare il test unitario per claculateTax() da EmployeeTests e creare una classe di test per ogni implementazione di InameHandler ?

Devo testare anche la classe Factory?

    
posta Songo 27.08.2012 - 09:35
fonte

5 risposte

6

I vecchi test vanno bene per verificare che calculateTax funzioni ancora come dovrebbe. Tuttavia, per questo non sono necessari molti casi di test, solo 3 (o forse alcuni di più, se si desidera testare anche la gestione degli errori, utilizzando valori imprevisti di name ).

Ciascuno dei singoli casi (al momento implementati in doSomething et al.) deve avere anche il proprio set di test, che testano i dettagli interni e casi speciali relativi a ciascuna implementazione. Nella nuova configurazione questi test potrebbero / dovrebbero essere convertiti in test diretti sulla rispettiva classe Strategy.

Preferisco rimuovere i vecchi test unitari solo se il codice che esercitano e la funzionalità che implementa, cessa completamente di esistere. Altrimenti, le conoscenze codificate in questi test sono ancora rilevanti, solo i test devono essere sottoposti a refactoring.

Aggiornamento

Potrebbe esserci qualche duplicazione tra i test di calculateTax (chiamiamoli test di alto livello ) e i test per le singole strategie di calcolo ( test di basso livello ) - dipende dalla tua implementazione.

Suppongo che l'implementazione originale dei test asserisca il risultato del calcolo delle imposte specifiche, verificando implicitamente che la strategia di calcolo specifica è stata utilizzata per produrla. Se si mantiene questo schema, si avrà effettivamente la duplicazione. Tuttavia, come suggerito da @ Kristof, è possibile implementare i test di alto livello utilizzando anche i mock, per verificare solo che il giusto tipo di strategia (fittizia) sia stato selezionato e invocato da calculateTax . In questo caso non ci sarà alcuna duplicazione tra test di livello alto e basso.

Quindi, se refactoring dei test interessati non è troppo costoso, preferirei il secondo approccio. Tuttavia, nella vita reale, quando eseguo un grande refactoring, tollero una piccola quantità di duplicazione del codice di test se mi fa risparmiare abbastanza tempo: -)

Should I test the Factory class too?

Di nuovo, dipende. Si noti che i test di calculateTax testano effettivamente la fabbrica. Quindi se il codice factory è un blocco switch banale come il tuo codice sopra, questi test potrebbero essere tutto ciò di cui hai bisogno. Ma se la fabbrica fa alcune cose più difficili, potresti voler dedicare alcuni test appositamente per questo. Tutto si riduce a quanti test è necessario per essere sicuri che il codice in questione funzioni davvero. Se, leggendo il codice o analizzando i dati di copertura del codice, vengono visualizzati percorsi di esecuzione non testati, dedicare alcuni test per esercitarli. Quindi ripeti fino a quando non sei completamente sicuro del tuo codice.

    
risposta data 27.08.2012 - 11:20
fonte
5

Inizierò dicendo che non sono esperto di TDD o test di unità, ma ecco come testarlo (userò il codice pseudo-like):

CalculateTaxDelegatesToNameHandler()
{
    INameHandlerFactory fakeNameHandlerFactory = Fake(INameHandlerFactory);
    INameHandler fakeNameHandler = Fake(INameHandler);

    A.Call.To(fakeNameHandlerFactory.getHandler("John")).Returns(fakeNameHandler);

    Employee employee = new Employee(fakeNameHandlerFactory);
    employee.CalculateTax("John");

    Assert.That.WasCalled(fakeNameHandler.calculateTax());
}

Quindi testerei che il metodo calculateTax() della classe dipendente richieda correttamente il suo NameHandlerFactory per un NameHandler e poi chiama il metodo calculateTax() del NameHandler restituito.

    
risposta data 27.08.2012 - 11:44
fonte
3

Stai prendendo una classe (dipendente che fa tutto) e crea 3 gruppi di classi: la fabbrica, il dipendente (che contiene solo una strategia) e le strategie.

Quindi crea 3 gruppi di test:

  1. Prova la fabbrica in isolamento. Gestisce gli input correttamente. Cosa succede quando passi in uno sconosciuto?
  2. Prova il dipendente da solo. Puoi impostare una strategia arbitraria e funziona come ti aspetti? Cosa succede se non ci sono strategie o set di fabbrica? (se è possibile nel codice)
  3. Prova le strategie in isolamento. Ognuno esegue la strategia che ti aspetti? Gestiscono gli ingressi dispari di confine in modo coerente?

Ovviamente puoi fare test automatici per l'intero shebang, ma ora sono più simili ai test di integrazione e dovrebbero essere trattati come tali.

    
risposta data 27.08.2012 - 14:22
fonte
2

Prima di scrivere qualsiasi codice, inizierei con un test per una fabbrica. Prendere in giro le cose di cui ho bisogno mi costringerei a pensare alle implementazioni e ai casi d'uso.

Di quanto vorrei implementare una Factory e continuare con un test per ogni implementazione e infine le implementazioni stesse per quei test.

Finalmente eliminerei i vecchi test.

    
risposta data 27.08.2012 - 10:40
fonte
2

La mia opinione è che non si dovrebbe fare nulla, nel senso che non si dovrebbero aggiungere nuovi test.

Sottolineo che questa è un'opinione e in realtà dipende dal modo in cui percepisci le aspettative dall'oggetto. Pensi che l'utente della classe vorrebbe fornire una strategia per il calcolo delle tasse? Se a lui non importa, i test dovrebbero riflettere questo, e il comportamento riflesso dai test unitari dovrebbe essere che non dovrebbero preoccuparsi che la classe abbia iniziato a usare un oggetto strategia per calcolare le tasse.

In realtà mi sono imbattuto in questo problema più volte quando si utilizza TDD. Penso che la ragione principale sia che un oggetto strategico non è una dipendenza naturale, al contrario di una dipendenza da confine architettonico come una risorsa esterna (un file, un DB, un servizio remoto, ecc.). Dal momento che non è una dipendenza naturale, di solito non baso il comportamento della mia classe su questa strategia. Il mio istinto è che dovrei cambiare i miei test solo se le aspettative della mia classe sono cambiate.

C'è un ottimo post dello zio Bob, che parla esattamente di questo problema quando si utilizza TDD.

Penso che la tendenza a testare ogni classe separata sia ciò che sta uccidendo TDD. L'intera bellezza di TDD è che usi i test per stimolare gli schemi di progettazione e non viceversa.

    
risposta data 02.12.2014 - 21:38
fonte

Leggi altre domande sui tag