Testy jednostkowe są podstawowym budulcem naszej pewności co do działania aplikacji. To dzięki nim jesteśmy w stanie szybko zweryfikować czy nasza pojedyncza jednostka programu działa zgodnie z tym co sobie założyliśmy. Jako jednostkę rozumiemy metodę, klasę a nawet cały pakiet. W pojedynczym teście wywołujemy dany element aplikacji i sprawdzamy czy uzyskany wynik (zwrócona wartość, stan obiektu czy też rzucony wyjątek) jest zgodny z oczekiwaniami. Ogromną zaletą testów jednostkowych jest ich pełna automatyzacja oraz wychwytywanie błędów na jak najwcześniejszym etapie dewelopmentu. Pytanie tylko co pomaga nam tworzyć testy jednostkowe w Javie? Odpowiedzią jest JUnit oraz AssertJ!

Testy jednostkowe w Junit

Junit
Testowanie jednostkowe z JUnit

Istnieją dwie wersje JUnit: 4 oraz 5. W związku z kilkoma pomyłkami popełnionymi przy implementacji JUnit 4 postała nowa, piąta wersja, która podzielona jest na trzy niezależne komponenty:

  • JUnit Platform - platforma do uruchamiania testów, którą wykorzystuje się pośrednio w IDE.
  • JUnit Jupiter - API niezbędne do pisania testów zawierające m.in. adnotacje @Test.
  • JUnit Vintage - API potrzebne do uruchamia testów napisanych w starszych wersjach JUnit.

Aby rozpocząć pracę musimy ściągnąć odpowiedni plik JAR z wybraną przez nas wersją. Najlepiej pobrać go z strony maven repository, jednak jeżeli używasz Mavena to musisz dodać odpowiednią zależność do pliku pom.xml. Ją również znajdziesz na podanej wcześniej stronie. Warto zaznaczyć, że scope ustawimy na test, ponieważ tą bibliotekę używamy tylko na potrzeby testów. Nie warto jest ją później pakować do paczki produkcyjnej.

1
2
3
4
5
6
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-api</artifactId>
  <version>5.8.0-M1</version>
  <scope>test</scope>
</dependency>

Przykład prostej asercji

Weźmy się od razu za napisanie pierwszego testu sprawdzającego czy dwie podane wartości są sobie równe. Jednak musimy mieć co testować, dlatego na początku utwórzmy przykładową klasę.

1
2
3
4
5
6
class JUnit {

  public String giveMeCommand(int id) {
    return id > 10 ? "Go rest!" : "March!";
  }
}

Przy pomocy testu jednostkowego sprawdzimy czy podając wartość większą od 10 do metody giveMeCommand otrzymamy literał “Go rest!”.

1
2
3
4
5
6
7
8
9
10
11
@Test
void should_order_a_rest_when_id_higher_then_10() {
  // given
  JUnit jUnit = new JUnit();

  // when
  String result = jUnit.giveMeCommand(11);

  // then
  assertEquals("Go rest!", result);
}

Test przechodzi. Użyliśmy tutaj metody statycznej assertEquals z klasy org.junit.jupiter.api.Assertions. Warto już teraz zaznaczyć, że pierwszym argumentem jest wartość oczekiwana, a drugim jest rezultat poddany weryfikacji. Jest to bardzo mylące i z tego powodu wolę pisać testy w AssertJ, ale o tym później. Jako trzeci argument możemy podać Stringa, który będzie wyświetlany jako opis testu, gdy asercja nie przejdzie. Zweryfikujmy to w następnym teście.

1
2
3
4
5
6
7
8
9
10
11
@Test
void should_order_to_march_when_id_higher_then_10() {
  // given
  JUnit jUnit = new JUnit();

  // when
  String result = jUnit.giveMeCommand(11);

  // then
  assertEquals("March!", result, "There is no place for lazybones!");
}

Tym razem test zaświecił się na czerwono. Sprawdźmy jaką informację otrzymaliśmy w konsoli.

org.opentest4j.AssertionFailedError: There is no place for lazybones! ==> 
Expected :March!
Actual   :Go rest!

Użycie asercji przy testach kolekcji

Sprawdźmy jak wyglądają testy kolekcji przy użyciu JUnit. Niestety nie jest to najwygodniejszy sposób pisania testów.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
void should_get_clubs_from_london_() {
  // given
  List<String> clubs = List.of(
      "Arsenal London", "Manchester United",
      "Liverpool F.C.", "Chelsea London"
  );

  // when
  List<String> result = clubs.stream()
      .filter(club -> club.contains("London"))
      .collect(toList());

  // then
  assertEquals(List.of("Arsenal London", "Chelsea London"), result);
}

Musimy ręcznie przefiltrować listę oraz przypisać ją do nowej zmiennej. Następnie w asercji trzeba ponownie utworzyć kolekcję oczekiwanych wartości i porównać ją z otrzymanym rezultatem. To strasznie dużo roboty!

A jak wyglądają testy wyjątków?

Przy testowaniu wyjątków w JUnit 4 musieliśmy pamiętać o kilku rzeczach. Trzeba było zdefiniować zasadę (adnotacja @Rule), która pozwalała nam na weryfikację typu wyjątku oraz jego wiadomości. Innym razem musieliśmy podać w adnotacji @Test jakiego wyjątku się spodziewamy. Było to bardziej eleganckie, ale nie mogliśmy sprawdzić wiadomości jaką miał w sobie wyjątek.

JUnit 5 poradził sobie z tym lepiej. Zobaczymy jaki efekt osiągnął.

1
2
3
4
5
6
7
8
9
10
11
@Test
public void should_should_throw_exception_containing_warning() {
  Exception exception = assertThrows(
      IllegalArgumentException.class, 
      () -> { throw new IllegalArgumentException(
          "Warning - there was passed wrong argument"
      );
  });

  assertTrue(exception.getMessage().contains("Warning"));
}

Nie jest źle. Musimy najpierw podać do assertThrows klasę wyjątku jakiego się spodziewamy, a następnie w wyrażeniu lambda wywołać testowany przez nas kod. Wynik przypisujemy do zmiennej Exception lub Throwable i sprawdzamy czy posiada prawidłową wiadomość.

Jeżeli jako pierwszy parametr w assertThrows podamy błędną klasę wyjątku otrzymamy komunikat.

org.opentest4j.AssertionFailedError: Unexpected exception 
type thrown ==> expected: <java.lang.NumberFormatException> but was: 
<java.lang.IllegalArgumentException>

Testy jednostkowe w AssertJ

Assertj
Testowanie jednostkowe z AssertJ

AssertJ powstał jako opensource’owa biblioteka, dzięki której możemy pisać testy w sposób “płynny” (później wyjaśnię na przykładzie o co dokładnie chodzi). AssertJ dostarcza wiele klas umożliwiających pisanie testów kodu w takich bibliotekach jak:

Oczywiście plik JAR z biblioteką możemy pobrać z maven repository bądź też poprzez dodanie odpowiedniej zależności do Mavena.

1
2
3
4
5
6
7
<dependency>
  <groupId>org.assertj</groupId>
  <artifactId>assertj-core</artifactId>
  <!-- use 2.9.1 for Java 7 projects -->
  <version>3.19.0</version>
  <scope>test</scope>
</dependency>

Przykład prostej asercji

Jeszcze raz przytoczę klasę, którą napisałem na potrzeby testów w JUnit.

1
2
3
4
5
6
class AssertJ {

  public String giveMeCommand(int id) {
    return id > 10 ? "Go rest!" : "March!";
  }
}

Oraz napiszemy ten sam test, tym razem przy pomocy AssertJ.

1
2
3
4
5
6
7
8
9
10
11
@Test
void should_order_a_rest_when_id_higher_then_10() {
  // given
  AssertJ assertJ = new AssertJ();

  // when
  String result = jUnit.giveMeCommand(11);

  // then
  assertThat(result).isEqualTo("Go rest!");
}

Test przechodzi, a my możemy zobaczyć czytelność asersji. Widzimy dokładnie co weryfikujemy oraz do jakiej wartości jest ona porównywana. Uzyskujemy to dzięki metodzie statycznej assertThat z klasy org.assertj.core.api.Assertions. Prawda, że jest to wygodniejsze niż w JUnit?

W AssertJ również istnieje możliwość zamieszczenia opisu do testu, gdy on nie przejdzie.

1
2
3
4
5
6
7
8
9
10
11
@Test
void should_order_to_march_when_id_higher_then_10() {
  // given
  AssertJ assertJ = new AssertJ();

  // when
  String result = assertJ.giveMeCommand(11);

  // then
  assertThat(result).as("There is no place for lazybones!").isEqualTo("March!");
}

Test zaświecił się na czerwono jak przewidywaliśmy, a w odpowiedzi otrzymujemy następującą informację.

org.opentest4j.AssertionFailedError: [There is no place for lazybones!] 
Expecting:
 <"Go rest!">
to be equal to:
 <"March!">
but was not.

Użycie asercji przy testach kolekcji

AssertJ biję tutaj JUnit na głowę. Pisanie testów kolekcji jest naprawdę przyjemne.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
void should_get_clubs_from_london_() {
  // given
  List<String> clubs = List.of(
      "Arsenal London", "Manchester United",
      "Liverpool F.C.", "Chelsea London"
  );

  // when/then
  assertThat(clubs)
      .filteredOn(club -> club.contains("London"))
      .containsOnly("Arsenal London", "Chelsea London");
}

Pisząc asercję możemy przefiltrować naszą kolekcję i napisać czego dokładnie oczekujemy po wykonaniu danej operacji. Jak dla mnie taki zapisy czyni ten kod łatwiejszym w czytaniu. Widać, że on po prostu “płynie”, można go wywoływać łańcuchowo np. tak jak poniżej.

1
2
3
4
5
6
7
8
assertThat(clubs)
    .filteredOn(club -> club.contains("London"))
    .contains("Arsenal London")
    .contains("Chelsea London")
    .doesNotContain("Manchester United")
    .hasSize(2)
    .doesNotHaveDuplicates()
    .doesNotContainNull();

Oczywiście jest to przesadzony przykład, ale pokazuje możliwości pisania asercji w AssertJ.

A jak wyglądają testy wyjątków?

AssertJ znaczenie upraszcza weryfikację wyjątków nawet w porównaniu z JUnit 5.

1
2
3
4
5
6
7
8
9
10
@Test
public void should_should_throw_exception_containing_warning() {
  assertThatThrownBy(() -> {
    throw new IllegalArgumentException(
        "Warning - there was passed wrong argument"
    );
  })
      .isInstanceOf(IllegalArgumentException.class)
      .hasMessageContaining("Warning");
}

Całość równie dobrze moglibyśmy zapisać w jednej linijce. Oczywiście nie byłoby to czytelne, dlatego lepiej jest gdzieniegdzie dodać entery.

Podsumowanie

Dało się pewnie odczuć, że moim zdecydowanym wyborem jest AssertJ. Wygrywa on po prostu czytelniejszym API, które ułatwia zapoznawanie się z napisanymi testami. Oczywiście możemy łączyć te dwie biblioteki osiągając naprawdę ciekawy rezultat, o którym niedawno się dowiedziałem.

1
2
3
4
5
6
assertAll(
    () -> assertThat("hot-dog").contains("dog"),
    () -> assertThat(List.of(1, 2, 3)).containsOnlyOnce(2),
    () -> assertThat(LocalDate.of(2020, 10, 10))
        .isBefore(LocalDate.of(2021, 10, 10))
);

Metoda statyczna assertAll pochodzi z JUnit, natomiast dobrze znany assertThat z AssertJ. Jak widać można wyciągnąć z każdej biblioteki co najlepsze i połączyć to w czytelne testy jednostkowe.

Jeżeli chcesz dowiedzieć się jak uzupełnić swoje testy o tworzenie zaślepek w Mockito zapraszam Cię do mojego poprzedniego artykułu.

A tym czasem na razie i cześć!

Źródła: