Come testare un'unità di una funzione che viene 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 refactoring questo per usare il Ploymorphism usando una classe di fabbrica e un modello di strategia:

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

Ora, se stessi 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 NameHandlerFactorye almeno 3 implementazioni di InameHandler.

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

Devo testare anche la classe Factory?

Risposte:


6

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

Ognuno dei singoli casi (al momento implementato in doSomethinget al.) Deve avere anche una propria serie di test, che testano i dettagli interni e i casi speciali relativi a ciascuna implementazione. Nella nuova configurazione questi test potrebbero / dovrebbero essere convertiti in test diretti sulla rispettiva classe di strategia.

Preferisco rimuovere i vecchi test unitari solo se il codice che esercitano e la funzionalità che implementa cessano completamente di esistere. Altrimenti, la conoscenza codificata in questi test è ancora rilevante, solo i test devono essere rifattorizzati.

Aggiornare

Potrebbero esserci dei duplicati 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.

Immagino che l'implementazione originale dei tuoi test affermi il risultato del calcolo fiscale specifico, verificando implicitamente che la strategia di calcolo specifica è stata utilizzata per produrlo. Se mantieni questo schema, avrai effettivamente la duplicazione. Tuttavia, come ha suggerito @Kristof, è possibile implementare i test di alto livello anche usando simulazioni, per verificare solo che sia stato selezionato e invocato il giusto tipo di strategia (finta) calculateTax. In questo caso non ci sarà duplicazione tra test di alto e basso livello.

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

Devo testare anche la classe Factory?

Di nuovo, dipende. Si noti che i test per calculateTaxtestare efficacemente la fabbrica. Quindi se il codice di fabbrica è un switchblocco 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, dopo aver letto il codice - o analizzato i dati di copertura del codice - vedi percorsi di esecuzione non testati, dedica qualche altro test per esercitarli. Quindi ripeti fino a quando non sei completamente sicuro del tuo codice.


Ho modificato un po 'il codice per avvicinarlo al mio attuale codice pratico. Ora è stato aggiunto un secondo input salaryalla funzione calculateTax(). In questo modo penso che duplicherò il codice di test per la funzione originale e le 3 implementazioni della classe di strategia.
Songo,

@Songo, per favore vedi il mio aggiornamento.
Péter Török,

5

Inizierò dicendo che non sono un esperto di TDD o test unitari, ma ecco come lo testerei (userò codice pseudo-simile):

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 calculateTax()metodo della classe Employee richiede correttamente la sua NameHandlerFactorya NameHandlere quindi chiama il calculateTax()metodo del reso NameHandler.


hmmmm quindi vuoi dire che dovrei invece fare del test un test comportamentale (testare che alcune funzioni sono state chiamate) e fare le asserzioni di valore sulle classi delegate?
Songo,

Sì, è quello che vorrei fare. Scriverei davvero test separati per NameHandlerFactory e NameHandler. Quando li hai, non c'è motivo di testare nuovamente la loro funzionalità nel Employee.calculateTax()metodo. In questo modo non è necessario aggiungere ulteriori test dei dipendenti quando si introduce un nuovo NameHandler.
Kristof Claes,

3

Stai prendendo una classe (impiegato che fa tutto) e creando 3 gruppi di classi: la fabbrica, l'impiegato (che contiene solo una strategia) e le strategie.

Quindi fai 3 gruppi di test:

  1. Testare la fabbrica in isolamento. Gestisce correttamente gli input. Cosa succede quando passi in uno sconosciuto?
  2. Testare il dipendente in isolamento. Puoi impostare una strategia arbitraria e funziona come previsto? Cosa succede se non esiste alcuna strategia o impostazione di fabbrica? (se è possibile nel codice)
  3. Prova le strategie in isolamento. Ognuno esegue la strategia che ti aspetti? Gestiscono gli input di confine dispari 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.


2

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

Quindi implementerei una fabbrica e continuerei con un test per ogni implementazione e infine le implementazioni stesse per quei test.

Alla fine rimuoverei i vecchi test.


2

La mia opinione è che non dovresti fare nulla, nel senso che non dovresti 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 imposte? Se non gli interessa, allora 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 di strategia per calcolare le tasse.

In realtà ho riscontrato questo problema diverse volte durante l'utilizzo di TDD. Penso che il motivo principale sia che un oggetto di strategia non è una dipendenza naturale, al contrario di dire una dipendenza dai confini architettonici 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 di zio Bob, che parla esattamente di questo problema quando si usa 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.

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.