Testowanie jednostkowe aplikacji to część wytwarzania software’u, która jest mi bardzo bliska. Lubię ją za to, że daje mi poczucie bezpieczeństwa (z mocnym akcentem na poczucie - wink! do Dawida Dęby) oraz możliwość szybkiego uruchomienia kodu, który właśnie piszę. Z tego powodu interesuje się dobrymi praktykami pisania testów. Oraz tymi złymi, aby ich unikać albo w szczególnych, uzasadnionych przypadkach, z nich skorzystać. Dzisiaj na tapet chciałem wziąć PowerMock, framework do testów pozwalający nam na wiele “ciekawych” rzeczy. A jakich? Przekonamy się za chwilę.

Jak działa PowerMock?

Na stronie projektu znajdziemy taki kawałek opisu.

… PowerMock uses a custom classloader and bytecode manipulation to enable mocking of static methods, constructors, final classes and methods, private methods, removal of static initializers and more. By using a custom classloader no changes need to be done to the IDE or continuous integration servers which simplifies adoption. …

Brzmi naprawdę poważnie. Ale na tym się nie kończy.

When writing unit tests it is often useful to bypass encapsulation and therefore PowerMock includes several features that simplifies reflection specifically useful for testing. …

Twórcy wyszli z założenia, że czasami warto obejść enkapsulację klasy na potrzeby testów korzystając z refleksji. Ale czy to dobry pomysł? Tutaj należy sobie odpowiedzieć samemu.

PowerMock w akcji

Dobra, przejdźmy do kodu. PowerMock na ten moment wspiera EasyMock i Mockito. My w przykładach skupimy się na tym drugim rozwiązaniu. Napiszmy sobie taki o to przykładowy kawałek kodu.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class BadDesignService {

  public final int calculateSomething(int a, int b) {
    return a * b * 32;
  }

  public static String processSomething(String value) {
    return value
        .replace("a", "@")
        .replace("A", "@")
        .replace("o", "@")
        .replace("O", "@")
        .replace("i", "!")
        .replace("I", "!");
  }

  public String generatePassword(String value) {
    return "password - " + doSomethingSecret(value);
  }

  private String doSomethingSecret(String value) {
    return Base64.getEncoder().encodeToString(value.getBytes(StandardCharsets.UTF_8));
  }

}

Mamy do dyspozycji metodę finalną, statyczną oraz prywatną. Zobaczymy w jaki sposób może je sobie zamockować.

Mockowanie konstruktura

Niech na pierwszy ogień pójdzie konstruktor. Ale zanim napiszemy klasę testową musimy dokonać kilku ceremoniałów. Jeśli korzystamy z Javy 21, tak jak ja, to będziemy musieli dodać opcję runtime o nazwie --add-opens, aby pozwolić PowerMock na modyfikację niepublicznych elementów poprzez refleksję. Można tego dokonać dodając odpowiednie linijki w Maven.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<build>
  <plugins>
    ..
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-surefire-plugin</artifactId>
      <configuration>
        <argLine>
          --add-opens=java.base/java.lang=ALL-UNNAMED
        </argLine>
      </configuration>
  	</plugin>
  </plugins>
</build>

Także klasy z pakietu java.lang znajdującego się w module java.base są teraz “otwarte” na modyfikację.

Kolejną walkę odbyłem z konfiguracją środowiska testowego. Miałem za mało cierpliwości, aby dopiąć użycie JUnit5, więc poprzestałem na starszym odpowiedniku tej biblioteki. Przy okazji niezbędnym krokiem okazał się też downgrade Mockito.

1
2
3
4
5
6
7
8
<dependencies>
  ..
  <dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>2.23.0</version>
  </dependency>
</dependencies>

Wersja 5.11.0 z zależności od PowerMock po prostu mi nie działała. Dostawałem informację: java.lang.NoClassDefFoundError: Could not initialize class org.mockito.Mockito. Jak więc żyć?

I po tym wszystkim możemy w końcu napisać nasz pierwszy test. Zwracam uwagę na adnotację @PrepareForTest. W niej podajemy klasy, które mają być uwzględniane przez PowerMock w kontekście danej klasy testowej.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RunWith(PowerMockRunner.class)
@PrepareForTest(BadDesignService.class)
public class BadDesignServiceWithPowerMockTest {

  @Test
  public void mockCreationOfBadDesignService() throws Exception {
    // given
    whenNew(BadDesignService.class).withNoArguments()
        .thenReturn(mock(BadDesignService.class));

    // when
    new BadDesignService();

    // then
    verifyNew(BadDesignService.class).withNoArguments();
  }

}

Od razu zaznaczę, że istotne jest importowanie klas i metod statycznych z pakietu org.powermock.api.mockito.*. Możemy przez przypadek zaciągnąć te przygotowane dla Mockito. W teście powyżej poprosiliśmy, aby aplikacja zwaracała nam przygotowanego mocka za każdym razem, gdy zostanie wywołany bezargumentowy konstruktor klasy BadDesignService. Następnie zweryfikowaliśmy czy faktycznie wywołanie tego konstruktora miało miejsce.

Mockowanie metod finalnych

Metoda calculateSomething jest finalna. Jeśli chcielibyśmy ją zamockować w klasycznym podejściu, tak jak poniżej, to dostaniemy naprawdę ładnie przedstawioną informację od Mockito, że nam się to nie udało. Powód: Also, this error might show up because: 1. you stub either of: final/private/equals()/hashCode() methods. ...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class BadDesignServiceWithMockitoTest {

  @Test
  public void mockFinalMethod() {
    // given
    BadDesignService badDesignService = mock(BadDesignService.class);
    // and
    when(badDesignService.calculateSomething(6, 6)).thenReturn(12);

    // when
    int result = badDesignService.calculateSomething(6, 6);

    // then
    assertEquals(12, result);
  }

}

W przypadku PowerMock nie będziemy z tym mieli najmniejszego problemu. Test będzie wyglądał identycznie, tylko importy będą inne.

Mockowanie metod statycznych

W przypadku metod statycznych musimy dołożyć pewną deklarację - mockStatic(BadDesignService.class). Wtedy już nic nie stoi nam na przeszkodzie, aby mockować statyczne metody znajdujące się w danej klasie.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void mockStaticMethod() {
  // given
  mockStatic(BadDesignService.class);
  // and
  when(BadDesignService.processSomething("abc")).thenReturn("ABC");

  // when
  String result = BadDesignService.processSomething("abc");

  // then
  assertEquals("ABC", result);
}

Mockowanie metod prywatnych

Metody prywatne to już wyższy stopień wtajemniczenia. Na poziomie kodu testu nie możemy z nich skorzystać. Z tego poziomu nie wiemy o ich istnieniu. Stąd API PowerMock wymaga od nas, aby nazwę metody prywatnej podać jako String w celu jej zamockowania. Z racji, że nie mamy jak wywołać metody prywatnej to na potrzeby przykładu musimy skorzystać z metody publicznej, która pod spodem korzysta z interesującej nas metody prywatnej.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void mockPrivateMethod() throws Exception {
  // given
  BadDesignService badDesignService = spy(new BadDesignService());
  // and
  doReturn("321").when(badDesignService, "doSomethingSecret", anyString());

  // when
  String result = badDesignService.generatePassword("123");

  // then
  assertEquals("password - 321", result);
}

Istotne z punktu widzenia tego test jest to, że nie możemy stworzyć mocka klasy, bo wtedy “wyczyścimy” jej wszystkie zachowania. Musimy skorzystać z innego elementu testowania jakim jest spy. W ten sposób tylko elementy, które chcemy zamockować będą zamockowane. O ile podamy to explicite. Reszta pozostanie bez zmian. Mocno to zawiłe, tak samo jak implementacja tego testu.

Dlaczego korzystanie z PowerMock boli?

Zakładam, że z jakiegoś powodu narzędzie jakim jest PowerMock powstało. Niemniej z mojego punktu widzenia nie jest ono potrzebne. Nawet potwierdza to też fakt, że na żadnym projekcie, którym byłem, nikt z niego nie korzystał. A byłem już na kilku. Być może czasem było to spowodowane brakiem świadomości istnienia takiego rozwiązania. Dobra, przydałoby się powiedzieć - dlaczego?

Pierwsze co przychodzi mi na myśl to, że z jakiegoś powodu ta enkapsulacja w danych elementach konstrukcyjnych została zastosowana. Autor kodu na pewno miał intencję stojącą za tym, aby czegoś nie udostępniać na świat zewnętrzny. Z pomocą PowerMock możemy natomiast to zmienić. W ten sposób mocno wgryzamy się w szczegóły implementacyjne, czego konsekwencją jest zabetonowanie kodu testami.

BTW, w Spock możemy bez żadnych dodatkowych narzędzi dostać się do prywatnych elementów klasy. Jedynie co to IntelliJ nas poinformuje, że przekroczyliśmy dany nam dostęp. Deal with it! 😎

Drugą rzeczą jest oparcie rozwiązania PowerMock na refleksji. Jest to konsekwencja pierwszego punktu, ale tutaj chciałbym zaznaczyć narzut wydajnościowy. Przy małej ilości testów nie jest to doskwierające, ale zakładam, że dla znacznie większej bazy testów możemy to już znaczniej odczuć.

Ostatnią kwestią jest ta wspomniana w poprzednim artykule. Dotyczy ona zawierania konstrukcji technicznych, które musimy dodać w teście, aby skorzystać z danego narzędzia. W przypadku PowerMock nie jest to jeszcze tak dotkliwe, ale też trzeba swoje napisać. Warto zwrócić dodatkowo uwagę na fakt, że samo dodanie PowerMock do projektu nie jest takie trywialne. Przynajmniej dla mnie nie było. Musiałem przejrzeć kilka wpisów na StackOverflow, aby tego dokonać.

Ale czy to narzędzie może się kiedyś przydać?

Może, kiedyś… Wydaje mi się, że znalazłby się jakiś uzasadniony przypadek. Wyobrażam sobie sytuację, w której do naszego projektu dodajemy zewnętrzną bibliotekę, z której wyjątkowo musimy skorzystać. Natomiast klasy w niej obecne są wątpiliwej jakości np. jedna z nich ma w konstruktorze zawartą logikę łączenia się z bazą danych. W takim przypadku chcielibyśmy zamockować tworzenie takiego obiektu, aby niepotrzebnie nie łączyć się z nią na potrzeby testów jednostkowych.

Tylko… moim zdaniem dobrą praktyką jest wypychanie zewnętrznych zależności na peryferia aplikacji. W tym celu opakowałbym problematyczną klasę w inną, która implementowałaby zaprojektowany przeze mnie interfejs. Wtedy małym narzutem implementacyjnym zyskałbym testowalność bez niepotrzebnych narzędzi oraz zabezpieczyłbym się przed ewentualną zmianą w zewnętrznej bibliotece, która mogłaby rozwalić moje rozwiązanie. Dodatkowo w razie czego mogę bez problemu podmienić taką bibliotekę na coś innego.

Podsumowanie

PowerMock to ciekawy projekt pod kątem rozwiązania technicznego. Jednak wydaje mi się, że jest z tych narzędzi, które powinny mieć łatkę “nie używać”. Być może nawet społeczeństwo programistyczne wydało już taki wyrok, ponieważ od 2 lat nie ma żadnego commita w projekcie na GitHubie…

I na zakończenie ważna informacja od samych twórców PowerMock…

Please note that PowerMock is mainly intended for people with expert knowledge in unit testing. Putting it in the hands of junior developers may cause more harm than good.

Oni sami ostrzegają dla kogo to narzędzie zostało stworzone. Ale i tak uważam, że im programista bardziej doświadczony tym mniej czuje potrzebę korzystania z takich narzędzi jak PowerMock. 😉