Testowanie metod prywatnych
Wstęp
Nie wdając się w długie rozważania, przyjmijmy na potrzeby tego tekstu, że testy jednostkowe piszemy, aby upewnić się, że stosunkowo mały fragment kodu (najczęściej pojedyncza klasa) nie zawiera błędów. Zadaniem każdej klasy jest służenie innym, tj. udostępnienie im pewnych wartościowych z ich punktu widzenia funkcjonalności, zatem i w testach skupiamy się na tym, czy owe dostępne z zewnątrz funkcjonalności są realizowane poprawnie (tj. zgodnie z dokumentacją i/lub ze zdrowym rozsądkiem).
I tutaj żadnych wątpliwości nie ma - takie funkcjonalności muszą zostać przetestowane. Wątpliwości pojawiają się, gdy developer odczuwa potrzebę przetestowania metod prywatnych, a więc takich, które z zewnątrz nie są bezpośrednio dostępne.
Do rozpatrzenia są tu dwa zagadnienia. Po pierwsze, czy testowanie metod prywatnych jest w ogóle dopuszczalne. Po drugie zaś, jak można to zrobić. W tym artykule postaram się udzielić odpowiedzi na oba te pytania.
Czy wypada?
Poglądy przeciwników testowania metod prywatnych podsumować można zwięzłym stwierdzeniem: "w kodzie obiektowym nietaktem jest zaglądanie obiektom w ich szczegóły implementacyjne". Jest to niewątpliwie słuszna uwaga. Osoby posługujące się tym argumentem, wskazują na słabość designu klasy, który nie pozwala programiście testować jej funkcjonalności jedynie poprzez wywołania metod publicznych.
Znani nam wszystkim pragmatyczni programiści (Dave Thomas & Andy Hunt) w książce Pragmatic Unit Testing we fragmencie poświęconym testom metod prywatnych, zauważają że:
W większości przypadków, klasę powinno się dać przetestować poprzez wywołanie jej metod publicznych. Jeżeli za prywatną lub chronioną metodą kryje się duża funkcjonalność, to być może jest to sygnał ostrzegawczy, który mówi ci, że w rzeczywistości ukryta jest tam odrębna klasa, która chciałaby wydostać się na zewnątrz.
Innymi słowy, uważają oni testowanie metod prywatnych za próbę rozwiązywania niewłaściwego problemu - prawdziwym problemem jest zły design klasy, nie zaś niemożność testowania jej metod prywatnych. Najwyraźniej podobnego zdania są twórcy JUnit, którzy nie zdecydowali się wprowadzić do swojego frameworku ułatwień do testowania metod prywatnych. Prawdopodobnie podejmując taką decyzję mieli na uwadze jedną z reguł XP odnoszącą się do testów:
Jeżeli coś jest zbyt trudne do przetestowania, to prawdopodobnie masz do czynienia z "code smell" - zmień to.
Jako przyczyny pisania testów dla metod prywatnych wskazuje się niekiedy programowanie typu code-first. Takie pisanie kodu sprawia, że przystępując do tworzenia testów programista ma już w głowie szczegóły implementacyjne klasy i zamiast myśleć o jej eksponowanym na zewnątrz zachowaniu, siłą rzeczy skupia swoją uwagę na jej wnętrzu. Jako odtrutkę na takie problemy przywołuje się Test Driven Development (TDD), a w szczególności kodowanie test-first.
Argumentem praktycznym wysuwanym przeciwko testowaniu metod prywatnych jest ich częstsza zmienność niż ma to miejsce w przypadku publicznego API, a co za tym idzie kruchość testów wymagających zmian w odpowiedzi na zmiany wprowadzane w implementacji klasy.
Argumenty drugiej strony są nie mniej ważkie, a ich najkrótsze podsumowanie stanowić może kolejne motto ze świata XP: Testuj wszystko, co może nie działać. Ponadto zwolennicy testowania metod prywatnych przedkładają kod przetestowany nad kod zgodny z pewnymi, mniej ich zdaniem istotnymi dla jakości kodu, zasadami programowania obiektowego. W dyskusji starają się zapędzić w kozi róg przeciwnika stawiając go przed alternatywami typu: "co jest ważniejsze dla ciebie - testy czy enkapsulacja?”. W ten sposób sugerują, że w niektórych przypadkach osiągnięcie obu tych celów jest niemożliwe, a wybór pomiędzy nimi może dać tylko jeden rezultat - kod musi być przetestowany i basta.
Z tym wywodem nie zgadzają się przeciwnicy testowania metod prywatnych (choć wielu z pewnością zgadza się ze stwierdzeniem, że testy są ważniejsze niż cała reszta dobrych praktyk) i jako lekarstwo przywołują wspominane już wcześniej TDD. Testy tworzone przy pomocy TDD sprawdzają całość stworzonego kodu jedynie poprzez wywołanie metod należących do jego API, zaś metody prywatne powstają w wyniku refaktoringu istniejących metod i jako takie, są również przetestowane. Scenariusz takiego developmentu, wygląda następująco:
- piszemy test dla metody A
- piszemy metodę A
- refaktorujemy
- piszemy test dla metody B
- piszemy metodę B
- refaktorujemy - zauważamy wspólną część metoda A i B, korzystając z refaktoringu extract method wyodrębniamy ją w postaci metody C
Skoro prywatna metoda C powstaje w wyniku refaktoringu w pełni przetestowanych metod A i B, to siłą rzeczy jest ona w całości przetestowana testami testującymi te (publiczne) metody, w związku z czym nie ma potrzeby pisać dla niej odrębnych testów.
Na to można usłyszeć, że owszem, że TDD jest bardzo przyjemną techniką, ale pomocną tylko wtedy, gdy tworzymy kod od podstaw. Tymczasem pisanie kodu od zera jest rzadkim luksusem, a proponowanie developerowi zmagającemu się z tysiącami linii legacy code techniki TDD nie jest żadnym wyjściem z sytuacji, i że wówczas testowanie metod prywatnych może być bardzo pomocne. W zetknięciu z wielką, rozdętą, wieloliniową i wszystkorobiącą klasą, developer ma prawo czuć się bezradny, zwłaszcza, że staje przed typowym problemem "jajka i kury":
- klasa ewidentnie powinna zostać zrefaktorowana, ale nie można tego zrobić, bo nie ma testów, które ustrzegą przed wprowadzeniem błędów podczas refaktoringu
- koniecznie trzeba napisać testy dla tej klasy, ale nie jest to możliwe bez refaktoringu
W tym momencie na forach internetowych rozlega się chór głosów pouczających co należy zrobić w takich wypadkach, począwszy od odesłania do literatury (zwłaszcza do "Working Effectively with Legacy Code" Michaela Feathersa) po różne mniej lub bardziej konkretne pomysły, które generalnie sprowadzają się do kuracji w postaci stopniowego pisania testów i refaktoryzacji aż do osiągnięcia zdrowego designu i w pełni przetestowanego kodu. Od tej chwili dyskusja już kompletnie odrywa się od początkowego tematu, pojawiają się wypowiedzi w stylu "w projekcie, w którym pracuję, nie mamy na to czasu", po czym rozmowa dryfuje w rejony technik tworzenia kodu i designu klas, zarządzania projektami IT, przeznaczaniem czasu na testy, narzekaniem na niedobrych project managerów itd.
Najwyraźniej w tym momencie obu stronom sporu o testowanie metod prywatnych kończy się amunicja, możemy więc pokusić się o małe podsumowanie:
- nikt nie twierdzi, że testowanie metod prywatnych to świetny pomysł - ale niektórzy twierdzą, że czasami jest to jedyne wyjście,
- wielu przedkłada kod przetestowany nad kod zgodny z OO - ale są i tacy, którzy akceptują wyłącznie kod, który jest przetestowany i zgodny z OO zarazem,
- tworząc kod od zera mamy luksus takiego pisania go, by mógł zostać w całości przetestowany jedynie poprzez wywołania jego metod publicznych - technika TDD może być pomocna w osiągnięciu takiego stanu,
- pracując z legacy code czasami nie mamy wyjścia - skoro przeciwnik (kod) gra nie fair, to i z naszej strony "wszystkie chwyty dozwolone" - włącznie z testowaniem metod prywatnych.
Załóżmy że wypada, to jak mam to zrobić?
Przed chwilą ustaliliśmy, że choć lepiej tego nie robić, to jednak czasami życie zmusza nas do testowania metod prywatnych. Nadszedł więc czas by zająć się techniczną stroną zagadnienia i opisać, jak można to zrobić.
W zasadzie istnieją dwie popularne i rozsądne techniki, oraz grono dość dziwnych (i na szczęście niepopularnych) pomysłów.
Metoda I - Zaduma, to znaczy refleksja
Najpopularniejszą chyba techniką jest zmiana modyfikatora dostępu poprzez mechanizm refleksji. Prywatna metoda tylko na czas testu staje się dostępna. Zaletą tej techniki jest brak konieczności dokonywania jakichkolwiek zmian w kodzie testowanej klasy. Fragment kodu testu odpowiedzialnego za "upublicznienie" i wywołanie metody wygląda tak:
Method method = targetClass.getDeclaredMethod(methodName, argClasses); method.setAccessible(true); return method.invoke(targetObject, argObjects);
Jak widać zamiast prostego wywołania metody, wyciągamy obiekt klasy Method
ją reprezentujący, ustawiamy mu dostęp, a następnie wywołujemy na nim metodę invoke.
Ten sposób dostępu do metod prywatnych jest na tyle popularny, że już dawno temu doczekał się swojej realizacji w postaci specjalnego dodatku do JUnit - JUnit-addons. Jego wadą jest wywoływanie metody poprzez podanie jej nazwy jako Stringu
, co powoduje kłopoty w przypadku refaktoringu testowanej klasy.
Metoda II - Domyślny modyfikator dostępu
Drugi pomysł opiera się na wykorzystaniu domyślnego modyfikatora dostępu. Zakładając, że testy leżą w takim samym pakiecie co testowana klasa (co jest dość powszechnie przyjętą praktyką), będą one miały dostęp do metod opatrzonych tym modyfikatorem. Zmieniamy więc modyfikator dostępu metody z prywatnego na domyślny i już możemy bez trudu pisać dla niej testy. Pytanie czy w ten sposób klasa nie odsłania zbytnio swojej implementacji? Sprawa jest dyskusyjna, ale biorąc pod uwagę, że domyślny modyfikator pozwala na dostęp do metody tylko klasom z tego samego pakietu, wiele osób nie uważa takiej zmiany za szkodliwą.
Słabością tej techniki jest konieczność modyfikacji kodu źródłowego - nie skorzystamy z niej, jeżeli nie mamy możliwości jego zmiany.
Pozostałe metody
Dla tych, którzy odczuwają niedosyt rozwiązań, podaje inne pomysły (zaprawdę, pomysłowość ludzka nie zna granic...):
- AspectJ – przy pomocy AspectJ można zrobić z grubsza wszystko, więc oczywiście można go użyć i w tym przypadku. Osobiście uważam to za słaby pomysł, przede wszystkim dlatego, że osób znających AspectJ ze świecą szukać, więc testy napisane z jego wykorzystaniem będą nieczytelne (a więc i nie do przerobienia) w przypadku, gdy jedyny specjalista AspectJ w całej firmie pójdzie na L4. Inna sprawa, że użycie AspectJ wydaje mi się tu przysłowiowym pójściem "z armatą na wróble".
- umieszczenie testów w statycznej klasie wewnętrznej testowanej klasy - owszem, taka klasa będzie miała dostęp do prywatnych metod, więc nadaje się do naszego celu. Ponieważ jednak nie chcę by klasa testowa trafiła do wynikowego JARa oraz nie podoba mi się pomysł mieszania kodu z testami, więc ten pomysł zdecydowanie odradzam.
- dla prywatnej metody foo()
dodajemy metodę foo_FOR_TESTING_ONLY()
, która ją wywołuje - i oczywiście umawiamy się z kolegami, że wolno z niej korzystać wyłącznie na potrzeby testów... eee... nie, dziękuję, testy w moim przekonaniu powinny służyć poprawianiu jakości kodu, a tu ewidentnie zaśmiecamy go. Poza tym na godzinę przed deadline nikt nie będzie przejmował się żadnymi ustaleniami i z metody foo_FOR_TESTING_ONLY()
ochoczo skorzysta, gdy tylko będzie tego potrzebował ...[po chwili zastanowienia] sam bym tak zrobił byle nie siedzieć po nocy.
Podsumowanie
Celem tego artykułu było omówienie zagadnienia testowania metod prywatnych. Przedstawiłem w nim główne argumenty wysuwane za i przeciw, jak również techniczne środki realizacji. Na koniec pozwolę sobie przedstawić własne zdanie na ten temat.
Krótko rzecz ujmując: nie powinieneś tego robić. Testowanie metod prywatnych w większości przypadków jest świadectwem słabości designu i może być wyeliminowane poprzez jego poprawę. Są jednak nieliczne sytuacje – zaliczam do nich np. przejściowe użycie testów metod prywatnych umożliwiające refaktoring szczególnie niechlujnie napisanej klasy – w których testy metod prywatnych uważam za całkowicie uzasadnione. Dlatego powinno się znać podstawowe techniki pisania takich testów – z użyciem refleksji i zmiany modyfikatora dostępu.
Linki
W artykule opierałem się na licznych blogach, wypowiedziach z forów internetowych i list mailingowych, fragmentach książek, tudzież własnych przemyśleniach, doświadczeniach i rozmowach, jakie miałem okazję prowadzić z kolegami po fachu. Poniżej przytaczam niekompletną listę źródeł, z której korzystałem w trakcie pisanie tekstu.
http://tech.groups.yahoo.com/group/junit/ - lista mailingowa projektu JUnit
http://junit.sourceforge.net/doc/faq/faq.htm - JUnit FAQ - How do I test private methods?
http://beust.com/weblog/archives/000303.html - Otaku, Cedric's weblog - Testing private methods? You bet.
http://agiletips.blogspot.com/2008/11/testing-private-methods-tdd-and-test.html - Paolo Caroli - Testing private methods, TDD and Test-Driven Refactoring
http://www.jroller.com/CoBraLorD/entry/junit_testing_private_fields_and - Arne Vandamme's weblog - JUnit testing private fields and methods
http://www.artima.com/suiterunner/private.html - Bill Venners - Testing Private Methods with JUnit and SuiteRunner
http://www.comp.mq.edu.au/units/comp229/resources/testingprivatemethods.html - COMP229 Object-Oriented Programming Practices
http://javaworks.wordpress.com/2007/09/06/q-how-do-you-unit-test-private-methods-using-junit/ - Q : How do you Unit Test private methods using JUnit
http://www.khussein.com/2008/tdd-testing-private-methods-using-aop/ - Khaled Hussein - Test Driven Development: Testing Private Methods Using Aspect Oriented Programming
Nobody has commented it yet.