Jak przetestować jednostkę, która jest refaktoryzowana do wzorca strategii?

10

Jeśli mam w kodzie funkcję, która wygląda następująco:

class Employee{

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

Normalnie przefakturowałbym to, aby użyć Ploymorfizmu przy użyciu fabrycznego wzorca klasy i strategii:

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

Teraz, gdybym używał TDD, miałbym kilka testów, które działają na oryginale calculateTax()przed refaktoryzacją.

dawny:

calculateTax_givenChrisSalaryBelowThreshold_Expect111(){}    
calculateTax_givenChrisSalaryAboveThreshold_Expect111(){}

calculateTax_givenDavidSalaryBelowThreshold_Expect222(){}   
calculateTax_givenDavidSalaryAboveThreshold_Expect222(){} 

calculateTax_givenScottSalaryBelowThreshold_Expect333(){}
calculateTax_givenScottSalaryAboveThreshold_Expect333(){}

Po refaktoryzacji będę miał klasę Factory NameHandlerFactoryi co najmniej 3 implementacje InameHandler.

Jak przejść do refaktoryzacji moich testów? Powinienem usunąć testów jednostkowych dla claculateTax()od EmployeeTestsi utworzyć klasę testową dla każdej implementacji InameHandler?

Czy powinienem również przetestować klasę Factory?

Songo
źródło

Odpowiedzi:

6

Stare testy są w porządku do sprawdzenia, czy calculateTaxnadal działa tak, jak powinno. Jednak nie potrzebujesz do tego wielu przypadków testowych, tylko 3 (a może trochę więcej, jeśli chcesz również przetestować obsługę błędów, używając nieoczekiwanych wartości name).

Każdy indywidualny przypadek (w tej chwili zaimplementowany w doSomethinget al.) Musi również mieć swój własny zestaw testów, które testują wewnętrzne szczegóły i szczególne przypadki związane z każdym wdrożeniem. W nowej konfiguracji testy te mogłyby / powinny zostać przekształcone w testy bezpośrednie w odpowiedniej klasie strategii.

Wolę usuwać stare testy jednostkowe tylko wtedy, gdy wykonywany przez nich kod i funkcjonalność, którą implementuje, całkowicie przestają istnieć. W przeciwnym razie wiedza zakodowana w tych testach jest nadal aktualna, tylko same testy muszą zostać ponownie opracowane.

Aktualizacja

Może wystąpić pewne powielanie między testami calculateTax(nazwijmy je testami wysokiego poziomu ) a testami poszczególnych strategii obliczeniowych ( testy niskiego poziomu ) - zależy to od implementacji.

Domyślam się, że oryginalna implementacja twoich testów potwierdza wynik konkretnego obliczenia podatkowego, domyślnie weryfikując, czy do jego wytworzenia użyto określonej strategii obliczeniowej. Jeśli utrzymasz ten schemat, rzeczywiście będziesz miał powielanie. Jednak, jak wskazał @Kristof, możesz zaimplementować testy wysokiego poziomu również za pomocą próbnych testów, aby sprawdzić, czy wybrano i wywołano odpowiedni rodzaj (próbnej) strategii calculateTax. W takim przypadku nie będzie duplikacji między testami wysokiego i niskiego poziomu.

Jeśli więc refaktoryzacja dotkniętych testów nie jest zbyt kosztowna, wolałbym to drugie podejście. Jednak w prawdziwym życiu, podczas masowego refaktoryzacji, toleruję niewielką ilość powielania kodu testowego, jeśli oszczędza mi to wystarczająco dużo czasu :-)

Czy powinienem również przetestować klasę Factory?

Znowu to zależy. Należy pamiętać, że testy calculateTaxskutecznie testują fabrykę. Jeśli więc kod fabryczny jest trywialnym switchblokiem, takim jak powyższy kod, testy te mogą być wszystkim, czego potrzebujesz. Ale jeśli fabryka robi bardziej skomplikowane rzeczy, możesz poświęcić kilka testów specjalnie dla tego. Wszystko sprowadza się do tego, ile testów potrzebujesz, aby mieć pewność, że dany kod naprawdę działa. Jeśli po przeczytaniu kodu lub analizie danych pokrycia kodu zobaczysz niesprawdzone ścieżki wykonania, poświęć kilka dodatkowych testów, aby je wykonać. Następnie powtarzaj tę czynność, aż będziesz w pełni pewien swojego kodu.

Péter Török
źródło
Zmodyfikowałem trochę kod, aby zbliżył się do mojego praktycznego kodu. Teraz dodano drugie wejście salarydo funkcji calculateTax(). W ten sposób myślę, że powielę kod testowy oryginalnej funkcji i 3 implementacje klasy strategicznej.
Songo
@ Songo, proszę zobaczyć moją aktualizację.
Péter Török
5

Zacznę od stwierdzenia, że ​​nie jestem ekspertem od TDD ani testów jednostkowych, ale oto jak bym to przetestował (użyję kodu pseudo-podobnego):

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

Więc sprawdziłbym, czy calculateTax()metoda klasy pracowniczej poprawnie pyta NameHandlerFactoryo a, NameHandlera następnie wywołuje calculateTax()metodę zwróconą NameHandler.

Kristof Claes
źródło
hmmmm, masz na myśli, że powinienem zamiast tego uczynić test testem behawioralnym (testowanie, że niektóre funkcje zostały wywołane) i uczynić twierdzenia o wartościach w delegowanych klasach?
Songo
Tak właśnie bym zrobił. Rzeczywiście napisałbym osobne testy dla NameHandlerFactory i NameHandler. Kiedy je masz, nie ma powodu, aby ponownie testować ich funkcjonalność w Employee.calculateTax()metodzie. W ten sposób nie musisz dodawać dodatkowych testów pracowników podczas wprowadzania nowego narzędzia NameHandler.
Kristof Claes
3

Bierzesz jedną klasę (pracownik, który robi wszystko) i tworzysz 3 grupy klas: fabrykę, pracownika (który zawiera tylko strategię) i strategie.

Zrób więc 3 grupy testów:

  1. Przetestuj fabrykę w izolacji. Czy poprawnie obsługuje dane wejściowe. Co się stanie, gdy zdasz nieznane?
  2. Przetestuj pracownika w izolacji. Czy potrafisz ustalić dowolną strategię i działa ona zgodnie z oczekiwaniami? Co się stanie, jeśli nie ma strategii ani ustawień fabrycznych? (jeśli jest to możliwe w kodzie)
  3. Przetestuj strategie w izolacji. Czy każdy realizuje oczekiwaną strategię? Czy obsługują nieparzyste dane graniczne w spójny sposób?

Oczywiście możesz wykonywać automatyczne testy dla całego shebang, ale teraz są one bardziej jak testy integracyjne i powinny być traktowane jako takie.

Telastyn
źródło
2

Przed napisaniem jakiegokolwiek kodu zacznę od testu dla fabryki. Kpiąc z potrzebnych rzeczy zmusiłbym się do myślenia o implementacjach i przypadkach użycia.

Następnie wdrożyłbym fabrykę i kontynuowałem test dla każdej implementacji, a na koniec same implementacje dla tych testów.

Na koniec usunę stare testy.

Patkos Csaba
źródło
2

Uważam, że nie powinieneś nic robić, co oznacza, że ​​nie powinieneś dodawać żadnych nowych testów.

Podkreślam, że jest to opinia, która w rzeczywistości zależy od tego, jak postrzegasz oczekiwania względem obiektu. Czy uważasz, że użytkownik klasy chciałby przedstawić strategię obliczania podatków? Jeśli go to nie obchodzi, testy powinny to odzwierciedlać, a zachowanie odzwierciedlone w testach jednostkowych powinno polegać na tym, że nie powinno ich obchodzić, że klasa zaczęła używać obiektu strategii do obliczania podatku.

Rzeczywiście napotkałem ten problem kilka razy podczas korzystania z TDD. Myślę, że głównym powodem jest to, że obiekt strategii nie jest naturalną zależnością, w odróżnieniu od zależności architektonicznej granicy, takiej jak zasób zewnętrzny (plik, baza danych, usługa zdalna itp.). Ponieważ nie jest to naturalna zależność, zwykle nie opieram zachowania mojej klasy na tej strategii. Odradzam instynkt, że powinienem zmieniać testy tylko wtedy, gdy zmieniły się oczekiwania mojej klasy.

Jest świetny post od wuja Boba, który mówi dokładnie o tym problemie podczas korzystania z TDD.

Myślę, że tendencja do testowania każdej oddzielnej klasy zabija TDD. Całe piękno TDD polega na tym, że używasz testów, aby pobudzić schematy projektowe, a nie odwrotnie.

Rafi Goldfarb
źródło