Czy powinienem testować moje podklasy lub abstrakcyjną klasę nadrzędną?

12

Mam szkieletową implementację, jak w punkcie 18 z Effective Java ( tutaj rozszerzona dyskusja ). Jest to klasa abstrakcyjna, która udostępnia 2 metody publiczne methodA () i methodB (), które wywołują metody podklas w celu „uzupełnienia luk”, których nie mogę zdefiniować w sposób abstrakcyjny.

Najpierw opracowałem go, tworząc dla niego konkretną klasę i pisząc testy jednostkowe. Gdy przyszła druga klasa, byłem w stanie wyodrębnić typowe zachowanie, zaimplementować „brakujące luki” w drugiej klasie i był on gotowy (oczywiście testy jednostkowe zostały utworzone dla drugiej podklasy).

Czas mijał i teraz mam 4 podklasy, z których każda implementuje 3 krótkie chronione metody, które są bardzo specyficzne dla ich konkretnej implementacji, podczas gdy implementacja szkieletowa wykonuje całą ogólną pracę.

Mój problem polega na tym, że kiedy tworzę nową implementację, piszę testy od nowa:

  • Czy podklasa wywołuje daną wymaganą metodę?
  • Czy dana metoda ma przypisaną adnotację?
  • Czy otrzymuję oczekiwane wyniki z metody A?
  • Czy otrzymuję oczekiwane wyniki z metody B?

Zobacz zalety tego podejścia:

  • Dokumentuje wymagania dotyczące podklasy poprzez testy
  • Może szybko zawieść w przypadku problematycznego refaktoryzacji
  • Przetestuj podklasę jako całość i za pomocą jej publicznego interfejsu API („czy metoda A () działa?”, A nie „czy ta chroniona metoda działa?”)

Problem, jaki mam, polega na tym, że testy nowej podklasy są w zasadzie nie wymagające: sprawdź część potwierdzającą testu. - Zazwyczaj podklasy nie mają specyficznych dla nich testów

Podoba mi się sposób, w jaki testy koncentrują się na wyniku samej podklasy i chronią ją przed refaktoryzacją, ale fakt, że wdrożenie testu stało się po prostu „pracą ręczną”, sprawia, że ​​myślę, że robię coś złego.

Czy ten problem występuje często podczas testowania hierarchii klas? Jak mogę tego uniknąć?

ps: Myślałem o przetestowaniu klasy szkieletu, ale wydaje się również dziwne tworzenie makiety klasy abstrakcyjnej, aby móc ją przetestować. I myślę, że każde refaktoryzowanie klasy abstrakcyjnej, które zmienia zachowanie, którego nie oczekuje się od podklasy, nie zostanie zauważone tak szybko. Moje wnętrzności mówią mi, że testowanie podklasy „jako całości” jest tutaj preferowanym podejściem, ale nie wahaj się powiedzieć, że się mylę :)

ps2: Poszukałem go i znalazłem wiele pytań na ten temat. Jednym z nich jest ten, który ma świetną odpowiedź od Nigela Thorne'a, moja sprawa byłaby jego „numerem 1”. Byłoby świetnie, ale w tym momencie nie mogę refaktoryzować, więc musiałbym żyć z tym problemem. Gdybym mógł dokonać refaktoryzacji, miałbym każdą podklasę jako strategię. To by było OK, ale nadal testowałbym „klasę główną” i testował strategie, ale nie zauważyłem żadnego refaktoryzacji, który łamałby integrację między klasą główną a strategiami.

ps3: Znalazłem kilka odpowiedzi stwierdzających, że „dopuszczalne jest testowanie klasy abstrakcyjnej. Zgadzam się, że jest to do przyjęcia, ale chciałbym wiedzieć, jakie jest preferowane podejście w tym przypadku (wciąż zaczynam od testów jednostkowych)

JSBach
źródło
2
Napisz abstrakcyjną klasę testową i odziedzicz po niej konkretne testy, odzwierciedlające strukturę kodu biznesowego. Testy mają na celu przetestowanie zachowania, a nie implementacji jednostki, ale to nie znaczy, że nie możesz wykorzystać swojej wiedzy o systemie do bardziej efektywnego osiągnięcia tego celu.
Kilian Foth,
Czytałem gdzieś, że nie należy używać dziedziczenia w testach, szukam źródła, aby przeczytać je uważnie
JSBach
4
@KilianFoth, czy jesteś pewien tej porady? To brzmi jak przepis na kruche testy jednostkowe, bardzo wrażliwy na zmiany w projekcie i dlatego nie bardzo łatwy do utrzymania.
Konrad Morawski

Odpowiedzi:

5

Nie widzę, jak piszesz testy dla abstrakcyjnej klasy podstawowej; nie możesz go utworzyć, więc nie możesz mieć jego instancji do przetestowania. Możesz stworzyć „sztuczną”, konkretną podklasę tylko na potrzeby jej testowania - nie jestem pewien, ile by to przyniosło. YMMV.

Za każdym razem, gdy tworzysz nową implementację (klasę), powinieneś pisać testy, które wykonują tę nową implementację. Ponieważ części funkcjonalności („luki”) są zaimplementowane w każdej podklasie, jest to absolutnie konieczne; wynik każdej z odziedziczonych metod może (i prawdopodobnie powinien ) być inny dla każdej nowej implementacji.

Phill W.
źródło
Tak, są różne (typy i treść są różne). Technicznie rzecz biorąc, byłbym w stanie wyśmiewać tę klasę lub mieć prostą implementację tylko do celów testowych z pustymi implementacjami. Nie podoba mi się to podejście. Ale fakt, że „nie muszę” myśleć, że przechodzę testy w nowych implementacjach, sprawia, że ​​czuję się dziwnie, a także zastanawiam się, czy jestem pewien w tym teście (oczywiście jeśli celowo zmienię klasę podstawową, testy kończą się niepowodzeniem, co jest dowodem na to, że mogę im ufać)
JSBach,
4

Zazwyczaj tworzysz klasę podstawową w celu wymuszenia interfejsu. Chcesz testować ten interfejs, a nie szczegóły poniżej. Dlatego powinieneś utworzyć testy, które utworzą instancję każdej klasy osobno, ale tylko za pomocą metod klasy bazowej.

Zaletą tej metody jest to, że ponieważ przetestowałeś interfejs, możesz teraz refaktoryzować interfejs. Może się okazać, że kod w klasie podrzędnej powinien być nadrzędny lub odwrotnie. Teraz twoje testy udowodnią, że nie złamałeś zachowania podczas przenoszenia tego kodu.

Zasadniczo powinieneś przetestować kod, jak ma być używany. Oznacza to unikanie testowania prywatnych metod i często oznacza, że ​​twoja testowana jednostka może być nieco większa niż początkowo sądziłeś - być może grupą klas, a nie pojedynczą klasą. Twoim celem powinno być izolowanie zmian, tak aby rzadko trzeba było zmieniać test, gdy refaktoryzujesz w danym zakresie.

Michael K.
źródło