Pracuję w branży IT od 6 lat i przez ten czas udało mi się, zarówno od strony testera, jak i dewelopera, zaznajomić z kilkoma frameworkami testowymi, dostępnymi dla Javy oraz brać udział w tworzeniu wewnętrznych rozwiązań na potrzeby testowania. Dziś opowiem o kilku najpopularniejszych – o przypadkach ich użycia, mocnych i słabych stronach, a także o różnicach na przykładach: JUnit, TestNG oraz Mockito.
Tu sprawa jest dość złożona. Na tę złożoność przede wszystkim wpływa najbardziej oczywista kwestia – testy. Warto zauważyć, że jest wiele możliwości testowania kodu – mamy unit testy, testy integracyjne, regresyjne, wydajnościowe, stress testy, manualne, automatyczne... Można również testować różne typy aplikacji – desktopowe, web, mobile, integracje pomiędzy nimi. Mógłbym tak wymienić jeszcze kilka linijek, ale lepiej po prostu zapamiętać i stosować się do pewnej zasady:
Używaj odpowiednich narzędzi do rozwiązania odpowiednich problemów.
Nie bez przyczyny słyszymy podobne wypowiedzi w trakcie trwania naszej kariery. I w tym wypadku jest to istotne. Kiedy decydujemy się na konkretny framework dla potrzeb naszych testów, chcemy umożliwić sobie i innym, którzy będą pracować przy danym projekcie:
A wszystko to jak najmniejszym kosztem. Zważając na fakt, że najczęściej nie ma uniwersalnego rozwiązania – dobrze jest wiedzieć, kiedy dane rozwiązanie będzie najlepsze.
W Ericsson dzięki takiemu podejściu obniżamy skomplikowanie projektów i koszt automatyzacji testów. Dla wewnętrznych zapotrzebowań firma tworzy również własne rozwiązania oparte na wymienionych frameworkach, znacznie ułatwiające pracę testerom i dające możliwość poznania nowych technologii.
Jeśli mówią Wam coś unit testy, od razu możecie się domyślić, jaki jest cel tego frameworka. JUnit jest to open source'owy szkielet służący do pisania powtarzalnych testów jednostkowych, czyli testów weryfikujących pojedyncze elementy oprogramowania, w wypadku Javy – klas, bądź metod. Weryfikacja ta w większości przypadków polega na porównywaniu wyniku testu z jakimś oczekiwanym wynikiem.
JUnit pomaga nam w tych aspektach, ale daje też dużo więcej możliwości, takich jak na przykład:
Framework cały czas się rozwija - najnowsza wersja JUnit 5 posiada wiele usprawnień w stosunku do poprzedniej wersji 4. Postaram się Wam więc dodatkowo - oprócz opisania samego frameworka - wskazać pomiędzy nimi różnice.
Od wersji JUnit 4 i wzwyż mamy możliwość skorzystania ze znanych nam z Javy 1.5 adnotacji:
W przypadku @Test – wskazujemy, że dana metoda jest metodą testową i zostanie uruchomiona w przypadku zainicjalizowania danej klasy testowej. Właśnie stworzyliśmy nasz pierwszy przypadek testowy.
Aby użyć daną adnotację, musimy zaimportować w klasie testowej odpowiednie klasy adnotacji znajdujące się w pakiecie biblioteki JUnit.
Inne przydatne adnotacje:
W JUnit weryfikacja testu odbywa się za pomocą asercji.
assertEquals (wiadomość, która pojawi się przy błędnej asercji, spodziewana_wartość_numeryczna, operacja_poddana_testowi)
W miejsce argumentu „operacja_poddana_testowi” można wstawić wywołanie swojej metody (pod warunkiem, że jej typ zwracany będzie odpowiadać typowi przyjmowanego argumentu):
assertEquals(„Addition”, 11, add(5, 6));
Jeśli dana asercja się nie powiedzie, tj. wynik będzie inny od oczekiwanego rezultatu – test wyrzuci błąd i zostaniemy o tym odpowiednio poinformowani.
JUnit 5
Najnowsza wersja JUnit 5 wprowadziła sporo zmian, względem swoich poprzedników. Do użycia wymaga wersji Javy 8 lub wyższej.
Sam framework został podzielony na trzy niezależne komponenty:
Najważniejszy z nich, czyli JUnit Jupiter, jest niezbędny do napisania testów. To właśnie w tym komponencie znajdują się wszelkiego rodzaju adnotacje i asercje.
Wraz z wersją piątą pojawiła się także bardzo przydatna adnotacja @Tag.
Dzięki niej mamy możliwość skategoryzować nasze testy i w danym przypadku uruchamiać tylko daną, konkretną grupę testów.
Testy napisane w Junit można uruchomić na kilka sposobów. Jeśli chcemy skorzystać z integracji z IDE, po dodaniu odpowiednich adnotacji (np. @Test) do klas i metod, pojawią się obok nich odpowiednie przyciski do uruchomienia metod, bądź całych klas testowych.
Inną możliwością jest uruchomienie testu przez wywołanie odpowiednich komend w konsoli. Tu też mamy dwie opcje – możemy albo uruchomić skompilowaną klasę testową tak jak zwykłą klasę napisaną w Javie:
% java -cp .:"/ścieżka_plików_testowych/" org.junit.runner.JUnitCore MyTest
podając jako argumenty ścieżkę do plików testowych, test runner JunitCore oraz nazwę klasy testowej bez rozszerzenia .java.
Drugą metodą jest skorzystanie z pluginów narzędzia do budowania projektu.
W zależności od tego, jakiego z nich używamy (np. Maven, Gradle, Ant), sposób konfiguracji i uruchomienia będzie nieco inny. Jest to jednak dużo elastyczniejsze rozwiązanie. Dzięki niemu za jednym razem możemy uruchomić wszystkie nasze testy (bądź takie, które wskażemy w konfiguracji).
Junit nie daje wielu możliwości, jeśli chodzi o wygodę tworzenia raportów. Wyniki i informacje o testach są eksportowane do plików XML. Dzięki nim mamy jedynie możliwość sformułowania raportów w oparciu o własne szablony. Aby wyeksportować je do jakiegoś bardziej przyjaznego użytkownikowi formatu, musimy zastosować zewnętrzne narzędzia.
Odpowiedź jest jedna – to zależy. Jeśli Twój projekt jest napisany w języku wcześniejszym, niż Java 8 – nie będziesz w stanie użyć JUnit 5. W przeciwnym wypadku będziesz mieć możliwość skorzystania z zalet, które oferuje najnowsza wersja:
Żeby nie było idealnie – najnowsze wersje oprogramowania często są mniej stabilne, nowe funkcjonalności nie zawsze perfekcyjnie współpracują z aktualnymi dostępnymi narzędziami, mogą mieć więcej nieodkrytych błędów. Aczkolwiek projekt ten jest na tyle dojrzały i popularny, iż w większości przypadków nie powinniśmy mieć od tej strony kłopotów. Jeśli tylko masz możliwość, polecam użyć najnowszej wersji frameworka.
TestNG to tak jakby rozwinięcie frameworka JUnit. Znajdziemy tu podobne zasady pisania testów, a także podobnie brzmiące adnotacje i asercje, które są wykorzystywane w tych samych celach. Podstawowym założeniem autorów przy tworzeniu TestNG było jedno – pokonać ograniczenia JUnit. Narzędzie miało zautomatyzować i umożliwić pisanie bardziej elastycznych i zaawansowanych testów w języku Java. Istotne jest również, iż dzięki swoim możliwościom, wspiera pisanie szerszego spektrum testów – funckcjonalnych, akceptacyjnych, integracyjnych i innych.
Framework ten powstał, gdy JUnit istniał w wersji 3 (nie posiadała ona jeszcze wielu udogodnień, np. adnotacji). Na tamte czasy TestNG oferował wiele innowacyjnych rozwiązań, między innymi wspomniane już adnotacje. Mamy tu znajome @Test, czy odpowiedniki JUnitowych:
@BeforeAll → @BeforeTest
@AfterAll → @AfterTest
@BeforeEach → @BeforeMethod
@AfterEach → @AfterMethod
Różnice może nie są wielkie przy pierwszym spojrzeniu, lecz w moim mniemaniu nazwy adnotacji TestNG nieco lepiej opisują ich działanie.
Tak jak w przypadku JUnit, testy możemy uruchomić zarówno przez IDE, jak i przez konsolę. Użytkownicy TestNG dostają jeszcze jedną bardzo ciekawą opcję – uruchamianie za pośrednictwem plików xml.
Wszystko to daje nam jeszcze lepszą elastyczność. Możemy na przykład stworzyć plik regressionTests.xml i zadeklarować w nim różne metody testowe z różnych klas:
Ogromną zaletą TestNG (jak również różnicą z JUnit do wersji 4) jest możliwość „zgrupowania” metod testowych, dzięki rozwinięciu adnotacji @Test. Dzięki temu możemy uruchomić tylko jakąś konkretną grupę testową. Nic oczywiście nie stoi na przeszkodzie, aby metoda należała do kilku grup:
@Test(groups = { "regression", "customGroup" })
Junit wcześniej nie dawał nam za wiele funkcji, jeśli chodzi o temat wielowątkowości testów. W zasadzie, patrząc na to szerzej, realnie nie daje nam w ogóle. Aby móc w ogóle skorzystać z tego rozwiązania, musielibyśmy stworzyć własną implementację.
TestNG rozwiązuje ten problem przez zastosowanie dodatkowych parametrów przy adnotacji @Test:
@Test(threadPoolSize = 4, invocationCount = 6)
threadPoolSize – ilość wątków przydzielona dla wywołania metody testowej. Może znacząco skrócić czas działania testu.
invocationCount – wartość oznaczająca, ile razy metoda testowa zostanie wywołana.
Wyobraźmy sobie, że chcielibyśmy zaimplementować testy, których wywołania będą uzależnione od przebiegu innych testów. I w tym przypadku TestNG ma dla nas odpowiednie rozwiązanie:
@Test(dependsOnMethods = { "dependOnThisMethod", “dependsOnThatMethod })
public void myTest()
{
dependOnThisMethod
to nic innego jak nazwa jednej z metod testowych, do których się odwołujemy. Tworzymy tu więc swego rodzaju następujący kontrakt:
Jeśli wszystkie metody testowe z bloku dependsOnMethods zakończą się sukcesem, uruchom tę metodę.
TestNG generuje raporty w wygodnej formie dokumentów html. Znajdują się tam informacje o: uruchomionych klasach testowych, metodach, lista udanych, nieudanych lub pominiętych testów oraz o czasie trwania całego testu, jak i jego części (metod).
Dane z raportu są również generowane do plików XML. Umożliwia to nam - tak jak w przypadku JUnit - tworzenie własnych raportów.
Framework ten, tak jak i wyżej wymienione, ułatwia pisanie testów jednostkowych.
Po co, skoro mam już dwa bardzo podobne do siebie?
Tu pozwolę posłużyć się pewnymi przykładami i na ich podstawie to wyjaśnić.
Wyobraźmy sobie, że dla testów potrzebujemy duży zbiór danych. Może to być testowa baza danych, zbiór zależności, czy też obiektów. Pojawiają się w tym momencie pewne zagrożenia co do zachowania jakości testów, zgodnie z trzecią zasadą testowania F.I.R.S.T.
Ten sam test uruchomiony wielokrotnie powinien za każdym razem dawać taki sam rezultat.
Korzystając z wyżej wymienionych typów danych, nasze testy nie będą zgodne z powyższą zasadą. Powinny być one niezależne od wszelkich zewnętrznych systemów, struktur danych i połączeń z nimi. Dzięki zachowaniu tej niezależności wiemy, że pewne zawahania systemu nie wpłyną nigdy na ich wyniki.
Pisząc testy jednostkowe, chcemy przetestować konkretną jednostkę systemu. Klasy w Javie, zgodnie z zasadą pojedynczej odpowiedzialności (Single Responsibility Principle from SOLID), powinny odpowiadać za jedną funkcjonalność. Często ta funkcjonalność jest zależna od innych klas, których instancji w danym przypadku tworzyć nie chcemy. Niestosując się do tych zasad:
Dzięki Mockito możemy “zamockować”, czyli stworzyć “udawane” przewidywalne obiekty, zamiast używać rzeczywistej implementacji. Na etapie mockowania twórca testu może określić zachowania danego obiektu, bez potrzeby uzupełniania wszystkich jego właściwości.
I w tym wypadku framework udostępnia nam szereg metod i adnotacji. Dzięki temu możemy użyć mockowania na dwa sposoby:
Jak zapewne zauważyliście, nie trzeba definiować właściwości obiektu service param. Dodatkowo dzięki powyższej implementacji, mamy pewność, że z każdym testem obiekt ten zostanie stworzony od nowa. Przykładowy serwis nie posiada wielu pól, ale w bardziej złożonej sytuacji – zysk będzie większy.
W tym przypadku zamiast adnotacji na klasie, tworzymy jej konstruktor, w którym użyjemy statycznej metody initMocks.
Niestety nie ma jednoznacznej odpowiedzi – nie istnieje „najlepszy framework testowy”. Dokonując wyboru, trzeba się zastanowić nad kilkoma rzeczami.
Nie każdy framework będzie się nadawał do każdego rodzaju testów.
Istnieją lepsze rozwiązania testowania funkcjonalnego dla np. aplikacji webowych (jak np Selenium), niż dla desktopowych. Nie oznacza to, że nie da się napisać takich testów w innych frameworkach, ale możemy być zmuszeni do stworzenia większej ilości kodu i zaciągnięcia dodatkowych zależności.
Do użycia niektórych frameworków nie wystarczy znajomość Javy i krótkie spojrzenie w dokumentację. Decydując się na trudniejsze rozwiązanie, zwiększamy poziom skomplikowania projektu.
Popularniejsze i mające dłuższą historię narzędzia z reguły są lepiej rozwijane, mają lepszą dokumentację i integrację z innymi frameworkami.
Porównując JUnit i TestNG w przypadku testów jednostkowych, powinniśmy wziąć pod uwagę ich różnice. Kilka lat temu byłbym skłonny stwierdzić, że TestNG będzie lepszym wyborem, ale deweloperzy JUnit skutecznie nadrobili braki względem największego konkurenta. Miejmy też na uwadze, że w danej sytuacji lepszym wyborem może być skorzystanie z więcej niż jednego rozwiązania.
poleciłbym JUnit 5 ze względu na jego lekkość, niski próg wejścia, łatwość w pisaniu testów i dobre wsparcie producenta. JUnit stał się pewnego rodzaju standardem w testowaniu jednostkowym między innymi z tych powodów.
to lepszym wyborem może się okazać TestNG. Miejmy jednak na uwadze, że autorzy JUnit są świadomi tych niedociągnięć i większość tych braków znajduje się już w ich roadmapie.
powinniśmy dodatkowo użyć Mockito.
To tylko niewielki pokaz możliwości wymienionych frameworków. Jeśli jesteście zainteresowani pogłębianiem wiedzy o nich, potrzebujecie dokumentacji, bądź chcecie bezpośrednio pobrać bibliotekę, to wszystko znajdziecie na: JUnit 5, TestNG i Mockito.
Witold Dzikowski w branży IT jest od 6 lat - początkowo jako Tester i Test Engineer, aktualnie - Java Developer w Ericsson.