TestNG, Mockito i TDD – Receptury

Jakiś czas temu w zakładzie wpadła mi w ręce książka Tomka Kaczanowskiego o wielce mówiącym tytule – “Practical Unit Testing with TestNG and Mockito”.  Opisanie tak szerokiego i interesującego zagadnienia jakim jest TDD niestety troszeczkę przerasta możliwości tego bloga (no dobra, nie mam po prostu czasu i skilla by wymądrzać się w tej dziedzinie). Natomiast Tomek jako praktyk stworzył ( moim absolutnie subiektywnym i skromnym zdaniem ) wspaniałe kompendium wiedzy na temat TDD, który każdy programista powinien przeczytać.

Nie zamierzam streszczać na blogu całej książki. Mija się to absolutnie z celem, albowiem Tomek  pisze bardzo fajnie (merytorycznie i z jajem) i głupotą byłoby przepisywanie wszystkiego ‘moimi słowami’. Myślę jednak, iż dla tych, którzy chcieliby liznąć TDD w skondensowanej postaci, albo też potrzebują miejsca, do którego mogliby wrócić kiedy czegoś zapomną – poniższe recepturki będą doskonałą rzeczą. Wybór pytań i zagadnień jest też oparty na moim doświadczeniu – wannabe TDD adept i procesu dodawania testów do starego projektu. O masie rzeczy nawet nie wspomniałem, ale mam nadzieję, że to wystarczy by poczuć o co chodzi.

Oczywiście zrobię też trochę reklamy – Tomek prowadzi bloga, swoje dzieło również zaopatrzył w oddzielny blog. Tamże można dowiedzieć się jak zakupić opisywaną książkę. Gdzieś mi również mignęło, że powstaje wersja polska – poczekamy, zobaczymy.

1. To o czym w ogóle rozmawiamy?

Rozmawiamy o testach jednostkowych. Małych i autonomicznych kawałeczkach kodu, który ma na celu sprawdzać poprawność działania pisanego kodu ( dopiero powstającego przy podejściu Test Driven Development, albo też zastanego legacy code).

Testy jednostkowe nie istnieją ‘w powietrzu’ – by spełniały swoje zadanie używamy różnych bibliotek i narzędzi do ich pisania. I tutaj sedno – mowa o TestNG (do testowania) oraz frameworku mockującym (nawet nie wiem jak to przetłumaczyć – mock to imitacja działającego obiektu) – Mockito.

2. Jak to uruchomić?

TestNG  jest po prostu biblioteką, którą można swobodnie pobrać i dodać do CLASSPATH naszej aplikacji. Oczywiście dziś w dobie IDE nikt raczej nie bawi się w uruchamianie JARów w konsoli. Istnieje plugin do Eclipsa, NetBeans i IDEA Intellij. Gdzieś tam przy okazji zapodział się Maven. U mnie zależności wyglądają tak:

<dependency>
 <groupId>org.testng</groupId>
 <artifactId>testng</artifactId>
 <version>6.3.1</version>
 <scope>test</scope>
 </dependency>
 <dependency>
 <groupId>org.mockito</groupId>
 <artifactId>mockito-all</artifactId>
 <version>1.9.0</version>
 <scope>test</scope>
 </dependency>

3. XML? Adnotacje? Rozszerzanie klas?

Zgódźmy się, że adnotacje to fajna rzecz i na tym poprzestańmy. Poniżej przykład prostego testu, który wykorzystuje większość podstawowych adnotacji TestNG.

import org.testng.annotations.Test;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.AfterTest;

public class SimpleTestNGTest {
  @Test
  public void f() {
      System.out.println("METODA F");
  }
  @Test
  public void f2() {
      System.out.println("METODA F2");
  }
  @Test
  public void f3() {
      System.out.println("METODA F3");
  }
  @BeforeMethod
  public void beforeMethod() {
      System.out.println("BEFORE METHOD");
  }

  @AfterMethod
  public void afterMethod() {
      System.out.println("AFTER METHOD");
  }

  @BeforeClass
  public void beforeClass() {
      System.out.println("BEFORE CLASS");
  }

  @AfterClass
  public void afterClass() {
      System.out.println("AFTER CLASS");
  }

  @BeforeTest
  public void beforeTest() {
      System.out.println("BEFORE TEST");
  }

  @AfterTest
  public void afterTest() {
      System.out.println("AFTER TEST");
  }

}

Efektem uruchomienia tego testu jest:

BEFORE TEST
BEFORE CLASS
BEFORE METHOD
METODA F
AFTER METHOD
BEFORE METHOD
METODA F2
AFTER METHOD
BEFORE METHOD
METODA F3
AFTER METHOD
AFTER CLASS
AFTER TEST

Mniej więcej widać co i jak.

4. Lubiłem JUnit i mam kupę takich testów? Co z nimi zrobić?
Tomek już coś takiego przerabiał. W sensie przerobił testy z jednego na drugie. Nawet to opisał. Do tego skreślił parę słów dlaczego TestNG jest fajniejsze niż JUnit.

5. Słyszałem trendy słowo – testy parametryzowane – wot te na?

Jest to miły ficzer, który umożliwia karmienie naszych metod testowych trochę większą ilością danych niż proste wywołania dla dwóch wartości w assertEquals. Umożliwia to adnotacja DataProvider. Metody oznaczone w ten sposób muszą zwracać tablicę tablic typu Object. Kod powie pewnie więcej:

import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

public class SimpleTestNGTest {

  @DataProvider(name="zrodloDanychNr1")
  public Object[][] dajDane() {
      return new Object[][] {
              {"key1","value1"},{"key2","value2"}
      };
  }

  @Test(dataProvider = "zrodloDanychNr1")
  public void f( String key, String value) {
      System.out.println("PARAMETRY: " + key + " - " + value );
  }

}

Efekt działania:

PARAMETRY: key1 – value1
PARAMETRY: key2 – value2

6. No dobra, ale co robi tak naprawdę to Mockito?

Dobre pytanie! Mockito służy do tworzenia imitacji działających obiektów. Kiedy powstaje kod w myśl TDD wpierw musimy stworzyć kod testów, zanim rozpoczniemy implementowanie logiki biznesowej (powiedzmy). Oczywiście by choćby próbować użycia klasy musimy ją wpierw stworzyć – inaczej kod w ogóle się nam nie skompiluje. Jednakże zamiast tworzyć od razu logikę możemy zasymulować jej działanie za pomocą składni Mockito.

Najlepiej działanie frameworka pokazać na przykładzie.


package com.wordpress.chlebik;

import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;

import org.mockito.Mockito;
import org.testng.annotations.Test;

public class SimpleTestNGTest {

  @Test
  public void sprawdzGracza() {

      Player pl = Mockito.mock(Player.class);

      when(pl.getName()).thenReturn( "Bochenek" );
      System.out.println("GRACZ: " + pl.getName() );

      verify(pl).getName();
      verify(pl).setName("Kajzerka");
  }

  class Player {

      String name = "Chlebik";

      public String getName() {
          return name;
      }

      public void setName( String name ) {
          this.name = name;
      }
  }

}

Dzieją się tutaj 2 istotne dla nas rzeczy – możemy sprawić by dany obiekt (wywołanie metody) zachował się dokładnie tak jak chcemy (nawet kiedy w ciele metody jasno deklarujemy, że ma zwrócić obecną w obiekcie wartość), a także możemy sprawdzić czy na pewno dany obiekt został wywołany dla konkretnych metod. Wykonanie powyższego testu skutkuje takim wynikiem:

GRACZ: Bochenek
FAILED: f
Wanted but not invoked:
player.setName(“Kajzerka”);
-> at com.wordpress.chlebik.SimpleTestNGTest.f(SimpleTestNGTest.java:25)

However, there were other interactions with this mock:
-> at com.wordpress.chlebik.SimpleTestNGTest.f(SimpleTestNGTest.java:22)

Jest to świetne narzędzie do imitacji działania obiektów – chcesz by obiekt klasy HttpServletRequest zwracał spreparowaną tablicę Cookie? Nic trudnego, stwórz ją i ustaw jako zwracaną przez ten obiekt. Zauważ, że dany obiekt nawet nie musi mieć metody ustawiającej daną właściwość! Cudowna rzecz. Weryfikacja choć nie widać tego na pierwszy rzut oka jest bardzo potężnym narzędziem. Sprawdzenie czy dana metoda została (lub nie) wywołana może służyć do bardzo jednoznacznego przetestowania flow naszej aplikacji. Ciekawy przykład takowego na StackOverflow.

Powyższe to naprawdę czubek góry lodowej. Zachęcam do zapoznania się z dokumentacją Mockito.

7. Dobrze by było gdyby te testy mogły od siebie zależeć. Po co mam uruchamiać 3 inne, skoro pierwszy podstawowy nie przeszedł?

Ależ proszę bardzo. Faktycznie wykonywanie operacji np. na obiekcie zapisanym w bazie danych kiedy nie udał się test wkładający tenże obiekt do bazy mija się trochę z celem. Procesor może i się nie zmęczy, ale po co dostawać masę komunikatów o błędach. TestNG radzi sobie całkiem dobrze z takim podejściem.

package com.wordpress.chlebik;

import org.testng.annotations.Test;

public class SimpleTestNGTest {

  @Test
  public void dodajGraczaDoBazy() throws Exception {
      // testowy kod zapisu gracza do bazy
      throw new Exception("Operacja nie powiodla sie!");
  }

  @Test(dependsOnMethods = { "dodajGraczaDoBazy" })
  public void sprawdzGracza() {

      System.out.println("Sprawdzamy gracza!");

  }

}

Po uruchomieniu zobaczymy takie coś.

FAILED: dodajGraczaDoBazy
java.lang.Exception: Operacja nie powiodla sie!
at com.wordpress.chlebik.SimpleTestNGTest.dodajGraczaDoBazy(SimpleTestNGTest.java:13)
uciety stacktrace

SKIPPED: sprawdzGracza

Jak widać po niepowodzeniu pierwszego testu drugi został w całości pominięty. Funkcjonalność ta ma z całą pewnością większe zastosowanie przy przeprowadzaniu testów integracyjnych czy też funkcjonalnych.

8. Eeeej, a co z wyjątkami?

Są 😉 Dają się też prosto obsługiwać. Wystarczy, że w przykładzie z poprzedniego punktu adnotacja @Test metody dodajGraczaDoBazy będzie wyglądała tak:

@Test(expectedExceptions = {Exception.class})

I obydwa testy zostaną uznane za poprawne.

9. Ok. Mamy jednakże XXI wiek i używam Springa. Da się to jakoś sklecić?

Rozumie się samo przez siebie. Od wersji 2.5 Spring do testowania używa funkcjonalności o nazwie Spring TestContext Framework. Framework ten potrafi ‘dogadać się’ z jakimkolwiek rozwiązaniem używanym do testów (JUnit, TestNG czy co kto tam sobie wymyśli). By wiedzieć co z czym polecam przeczytanie tego tutoriala o testach integracyjnych. Niestety jeśli chcemy dostać się do kontekstu Springa jesteśmy zmuszeni dziedziczyć po klasie AbstractTestNGSpringContextTests. Z drugiej jednak strony rzecz może dotyczyć tylko testów integracyjnych – jednostkowe powinny być samodzielne i autonomiczne, a wszystkie zależności możemy zmockować. O testach funkcjonalnych oraz o testowaniu samego widoku w Spring MVC na pewno jeszcze napiszę.

To tyle. Mam nadzieję, że w toku dalszych walk z TDD dorzucę jeszcze parę punktów do powyższej listy. Jeśli sami macie pomysł – piszcie na mejla lub w komentarzach.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s