Diversity w polskim IT
Witold Dzikowski
Ericsson
Witold DzikowskiJava Developer @ Ericsson

Frameworki testowe w Javie

Na przykładach JUnit, TestNG i Mockito poznaj przypadki użycia frameworków testowych w Javie, ich mocne i słabe strony oraz dowiedz się, jakie różnice je dzielą.
14.05.201910 min
Frameworki testowe w Javie

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.

Kilka najpopularniejszych? Ale dlaczego jest ich aż tyle?

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:

  • łatwiejsze pisanie testów,
  • skuteczniejsze testowanie,
  • tworzenie przejrzystych dla użytkownika raportów.


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.

JUnit

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:

  • wiele sposobów uruchamiania testów,
  • oddzielanie testów od kodu produkcyjnego,
  • tworzenie przypadków i scenariuszy testowych,
  • możliwość tworzenia raportów dzięki wygenerowanym plikom XML,
  • integrację z różnymi środowiskami programowania.


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.

Adnotacje

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:

  • @Disableduniemożliwia uruchomienie danej klasy testowej bądź metody.
  • @BeforeEach – wskazuje, iż dana metoda zostanie uruchomiona przed każdą metodą testową.
  • @AfterEach – wskazuje, iż dana metoda zostanie uruchomiona po każdej metodzie
    testowej.
  • @BeforeAll – wskazuje, iż dana metoda zostanie uruchomiona przed wszystkimi metodami testowymi.
  • @AfterAll – wskazuje, iż dana metoda zostanie uruchomiona po wszystkich metodach testowych.


Asercje

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:

  • JUnit Platform - to platforma do uruchamiania testów (jeśli uruchamiamy testy w naszym IDE, korzystamy z tego komponentu).
  • JUnit Jupiter - jako API, które jest używane do pisania testów.
  • JUnit Vintage- to API umożliwiające uruchamianie testów napisanych w starszych wersjach Junit.


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.

Grupy testowe

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.

Uruchomienie 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).

Raportowanie

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.

Którą wersję wybrać?

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:

  • nowe adnotacje (np. @Tag),
  • wsparcie dla wyrażeń Lambda, co umożliwi pisanie krótszego kodu,
  • kompatybilność wsteczna względem poprzednich wersji,
  • lepsze wsparcie twórców, zarówno pod kątem nowych funkcjonalności, jak i naprawy błędów.


Ż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

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.

Uruchomienie testów

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:


Grupy testowe

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" })


Wielowątkowość

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.


Zależności testów

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ę.

Raportowanie

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.

Mockito

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:

  • nasze testy jednostkowe mogą stać się testami integracyjnymi,
  • testy będą zależeć od stanu zewnętrznych usług bądź struktur danych,
  • testy mogą otrzymywać różne dane, w zależności od momentu ich odpytywania,
  • dłuższy czas wykonywania testów – korzystanie z zewnętrznych systemów, API - będzie wolniejsze, niż użycie danych stworzonych podczas testu.

To po co to Mockito?

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:

  • @Runwith – deklarujemy, że dana klasa będzie klasą mockującą.
  • @Mock – inicjalizacja mockowanych obiektów.
  • @Before – wskazujemy metodę, która zostanie uruchomiona przed każdym testem.
  • @Test – wkazujemy metodę testową.


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.

Metoda initMocks

W tym przypadku zamiast adnotacji na klasie, tworzymy jej konstruktor, w którym użyjemy statycznej metody initMocks.

Który framework testowy wybrać?

Niestety nie ma jednoznacznej odpowiedzi – nie istnieje „najlepszy framework testowy”. Dokonując wyboru, trzeba się zastanowić nad kilkoma rzeczami.

Jakiego rodzaju testy chcemy pisać?

Nie każdy framework będzie się nadawał do każdego rodzaju testów.

Jakiego rodzaju aplikacje będą testowane?

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.

Próg wejścia i stopień trudnoś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.

Wsparcie producenta

Popularniejsze i mające dłuższą historię narzędzia z reguły są lepiej rozwijane, mają lepszą dokumentację i integrację z innymi frameworkami.

Szybkość działania, koszt wdrożenia


Co jeszcze wziąć pod uwagę?


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.

Jeśli:

  • mamy dość proste scenariusze testowe,
  • nie zależy nam na wbudowanym rozwiązaniu wielowątkowości testów,
  • nasz kod produkcyjny nie wyróżnia się dużym stopniem skomplikowania i wieloma poziomami abstrakcji,
  • nie potrzebujemy gotowych raportów html,
  • nasz projekt korzysta z wersji Java 8 lub wyższej,


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.

Jeśli:

  • potrzebujemy wygodnego implementowania wielowątkowości testów,
  • zależy nam na gotowych raportach html,
  • chcemy rozszerzyć scope testów o np. integracyjne, funkcjonalne, regresyjne,


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.

Jeśli:

  • kod produkcyjny naszego projektu jest dość skomplikowany („ciężkie obiekty”, dużo fabryk),
  • funkcjonalności mają jakąś pośrednią warstwę, która może wpłynąć na wyniki testów, korzystają z innych systemów, struktur danych, serwisów,


powinniśmy dodatkowo użyć Mockito.

Podsumowanie

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.

O autorze

Witold Dzikowski w branży IT jest od 6 lat - początkowo jako Tester i Test Engineer, aktualnie - Java Developer w Ericsson.

<p>Loading...</p>