Maven ma wiele ciekawych pluginów, które automatyzują nam powtarzalne czynności. Nie inaczej sprawa ma się w przypadku testowania jednostkowego naszej aplikacji. W tym artykule chciałbym zapoznać Cię z bardzo przydatnym pluginem - Surefire. Jego głównym zadaniem jest uruchamianie wcześniej wspomnianych testów jednostkowych przy użyciu jednej komendy. Przejdźmy zatem do sprawdzenia w jaki sposób możemy zarządzać pluginem Surefire oraz jak go prawidłowo skonfigurować.

Co w Surefire piszczy?

Domyślnie plugin Surefire podpięty jest jako jeden z elementów wywołania fazy test głównego lifecycle Mavena. Zamysłem powstania Surefire było wykonywanie wszystkich testów jednostkowych w wybranym projekcie w możliwie jak najprostszy sposób - przez wywołanie jednej komendy. Efektem finalnym działania pluginu Surefire jest wyświetlenie w konsoli wyniku uruchomienia testów jednostkowych oraz wygenerowanie raportu w postaci dwóch plików - TXT oraz XML. Jeśli nic nie zmienimy w konfiguracji POM to znajdziemy je w katalogu ${basedir}/target/surefire-reports. Istnieje również możliwość otrzymania wyniku testów w formacie HTML, ale to wymaga dodatkowego nakładu pracy. Jednak na końcu tego artykułu zahaczymy i o ten temat.

Przygotowanie projektu

Na samym początku musimy stworzyć przykładowy projekt Maven, na którym zweryfikujemy działanie naszego dzisiejszego bohatera. Zachęcam Cię do zajrzenia do artykułu pokazującego w jaki sposób możemy to uczynić. Ja zdecydowałem się na sposób wykorzystujący IDE - IntelliJ, ponieważ według mnie jest to najszybszy sposób kreowania testowego schematu projektu.

W pierwszej kolejności oczywiście musimy dodać plugin Surefire do POM. Robimy to poprzez wykorzystanie ścieżki project.build.plugins. Dobrą praktyką jest zdefiniowanie explicite z jakiej wersji będziemy korzystać. Dlatego w naszym projekcie użyjemy ówcześnie najnowszej wersji 3.0.0-M5. Dodatkowo zmienimy od razu domyślną konfigurację. Teraz Surefire będzie szukał tylko plików klas, których nazwa kończy się na Spec lub Test.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<project>
  ...
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.0.0-M5</version>
        <configuration>
          <includes>
            <include>**/*Spec.*</include>
            <include>**/*Test.*</include>
          </includes>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Dobrze by było stworzyć jakąś klasę biznesową, którą poddamy testom. Nie musi to być nic skomplikowanego, ponieważ chcemy się skupić tylko i wyłącznie na działaniu pluginu. Tworzymy, więc klasę o nazwie BusinessClass, która posiada metodę mającą za zadanie łączyć, za pomocą spacji, przekazaną tablicę typu String w jeden literał. Dodatkowo dodamy do naszej klasy jeszcze dwie pomocnicze metody - run i stop, które będziemy uruchamiać przed i po wykonaniu każdego z testów.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BusinessClass {

  public String concat(String... words) {
    return String.join(" ", words);
  }

  public void run() {
    System.out.println("Starting....");
  }

  public void stop() {
    System.out.println("Stopping....");
  }
}

Dostępne goals dla Surefire

Gdy już mamy stworzony projekt możemy skorzystać z polecenia, które zostało przedstawione w jednym z wcześniejszych artykułów. Chodzi o możliwość sprawdzenia jakie goals są dostępne w wybranym pluginie - w naszym przypadku w Surefire. W tym celu należy uruchomić konsolę i wpisać następujące polecenie mvn surefire:help będąc w głównym katalogu projektu.

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
26
27
28
$ mvn surefire:help
[INFO] Scanning for projects...
[INFO]
[INFO] -----------------< pl.devcezz:maven-surefire >-----------------
[INFO] Building maven-surefire 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-surefire-plugin:3.0.0-M5:help (default-cli) @ maven-surefire ---
[INFO] Maven Surefire Plugin 3.0.0-M5
  Maven Surefire MOJO in maven-surefire-plugin.

This plugin has 2 goals:

surefire:help
  Display help information on maven-surefire-plugin.
  Call mvn surefire:help -Ddetail=true -Dgoal=<goal-name> to display parameter
  details.

surefire:test
  Run tests using Surefire.


[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.508 s
[INFO] Finished at: 2021-11-25T14:18:29+01:00
[INFO] ------------------------------------------------------------------------

Otrzymujemy informację, że Surefire zawiera dwa goals, a właściwie jeden (nie liczymy help). Jest nim test, którego zadaniem jest właśnie uruchamianie testów jednostkowych. Jak już wspomniałem wcześniej, ten goal standardowo podpięty jest pod phase test dla domyślnego lifecycle. Wykona się on przy wywołaniu komendy mvn test lub jednego z późniejszych phase.

Warto przy okazji spojrzeć na podpowiedź jaką otrzymaliśmy w konsoli. Wpisując polecenie mvn surefire:help -Ddetail=true -Dgoal=test wyświetli nam się lista wszystkich dostępnych parametrów wraz z ich szczegółami. W niej znajdzie się między innymi opis użytego wcześniej w POM includes, z którym polecam się zapoznać.

Korzystanie z różnych dostawców bibliotek testowych

W katalogu z testami możemy umieścić dowolną ilość klas testowych korzystających z następujących bibliotek.

  • TestNG
  • JUnit (3.8, 4.x or 5.x)
  • Spock
  • Cucumber
  • czy zwykłe testy oparte o POJO

Dla Surefire nie ma żadnego znaczenia czy zdecydowaliśmy się na np. JUnit czy TestNG. Zadziała on tak samo niezależnie od tego z jakiego dostawcy skorzystaliśmy, bez potrzeby jego wskazywania w ustawieniach pluginu. Wystarczy, więc dodać odpowiednią zależność wybranej biblioteki testowej do POM, napisać testy jednostkowe i wywołać mvn test w konsoli. Warto nadmienić, że wszystkie parametry konfiguracyjne Surefire są wspierane przez każdą z wyżej wymienionych bibliotek.

Muszę napisać słowo komentarza na temat POJO. Istnieje możliwość napisania testów, które nie wykorzystują żadnych zewnętrznych rozwiązań. Opierają się one tylko na podstawowej składni Javy. Trzeba jedynie przypomnieć sobie o słowie kluczowym assert. Wymagane jest natomiast odpowiednie nazywanie publicznych metod testowych (w publicznej klasie testowej) rozpoczynając je od słowa test*. Jeśli chcielibyśmy jeszcze, aby coś wykonało się przed i po każdym teście możemy dodać dwie specjalne metody - setUp oraz tearDown. Przejdźmy zatem do sprawdzenia jak to co na napisałem prezentuje się w praktyce.

Testy napisane w POJO

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

  BusinessClass businessClass = new BusinessClass();

  public void setUp() {
      businessClass.run();
  }

  public void testInPojo1() {
      System.out.println("First test");
      assert "Java".equals(businessClass.concat("Java"));
  }

  public void testInPojo2() {
      System.out.println("Second test");
      assert "Java 17".equals(businessClass.concat("Java", "17"));
  }

  public void tearDown() {
      businessClass.stop();
  }
}

Napisaliśmy dwa proste testy, które sprawdzają czy wybrane słowa odpowiednio się połączą w jeden literał. Skorzystaliśmy również z możliwości dodania dwóch specjalnych metod, o których napisałem wyżej. Dzięki nim przed i po każdym teście zostaną uruchomione dwie metody klasy BusinessClass - run oraz stop. Otwórzmy terminal i uruchommy te testy.

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
26
$ mvn test
...
[INFO] --- maven-surefire-plugin:3.0.0-M5:test (default-test) @ maven-surefire ---
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running PojoTest
Starting....
Second test
Stopping....
Starting....
First test
Stopping....
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.013 s - in PojoTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.448 s
[INFO] Finished at: 2021-11-29T15:15:24+01:00
[INFO] ------------------------------------------------------------------------

Tak jak zakładaliśmy, wszystkie testy przeszły. W konsoli otrzymaliśmy również potwierdzenie, że metody setUp i tearDown wykonały się dla każdego testu. Pozostaje nam jeszcze weryfikacja co się stanie jeśli dana asercja zwróci nam pewną nieprawidłowość działania naszego kodu.

1
2
3
4
5
6
7
8
9
  ...

  public void testInPojo3() {
      System.out.println("Third test");
      assert "Java".equals(businessClass.concat("Java", "17"));
  }

  ...
}
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
26
27
28
29
30
31
32
33
34
35
36
37
38
$ mvn test
...
[INFO] --- maven-surefire-plugin:3.0.0-M5:test (default-test) @ maven-surefire ---
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running PojoTest
Starting....
First test
Stopping....
Starting....
Third test
Stopping....
Starting....
Second test
Stopping....
[ERROR] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.021 s <<< FAILURE! - in PojoTest
[ERROR] PojoTest.PojoTest.testInPojo3()  Time elapsed: 0.004 s  <<< FAILURE!
java.lang.AssertionError
        at PojoTest.testInPojo3(PojoTest.java:21)

[INFO]
[INFO] Results:
[INFO]
[ERROR] Failures: 
[ERROR]   PojoTest#testInPojo3 
[INFO]
[ERROR] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  4.058 s
[INFO] Finished at: 2021-11-29T15:19:01+01:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.0.0-M5:test (default-test) on project maven-surefire: There are test failures.
...

Dostaliśmy informację o tym, że test o nazwie testInPojo3 się nie powiódł. Nie wiemy natomiast z czego to wynika. Korzystając z zewnętrznych bibliotek dowiedzielibyśmy się, że wynikowy String metody concat to Java 17, a my oczekiwaliśmy tylko Java. W przypadku POJO nie jest to tak przejrzyste. Sprawdźmy zatem jak TestNG radzi sobie z Surefire.

Testy napisane w TestNG

Dodajmy identyczne testy z użyciem TestNG. Oczywiście na początku musimy uwzględnić tą bibliotekę w pom.xml.

1
2
3
4
5
6
7
8
9
10
11
12
  ...
  <dependencies>
    <dependency>
      <groupId>org.testng</groupId>
      <artifactId>testng</artifactId>
      <version>7.4.0</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  ...
</project>

Zatrzymajmy się na chwilę w tym miejscu, zaraz po dodaniu zależności do POM. Wywołajmy ponownie komendę mvn test w głównym katalogu projektu.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ mvn test
...
[INFO] --- maven-surefire-plugin:3.0.0-M5:test (default-test) @ maven-surefire ---
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running PojoTest
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.369 s - in PojoTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.805 s
[INFO] Finished at: 2021-11-29T15:56:35+01:00
[INFO] ------------------------------------------------------------------------

Nagle Surefire nie znajduje żadnych testów z PojoTest! Wystarczyło dołączyć TestNG do projektu, aby nasze wcześniej napisane testy się nie wywołały. Jest to raczej ciekawostka, ponieważ zakładam, że nikt nie pisze już testów jednostkowych bez korzystania z zewnętrznych rozwiązań. Ruszajmy dalej!

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
26
27
28
29
30
31
32
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import static org.testng.Assert.assertEquals;

public class TestNGTest {

  BusinessClass businessClass = new BusinessClass();

  @BeforeMethod
  public void setUp() {
    businessClass.run();
  }

  @Test
  public void testInTestNG1() {
    System.out.println("First test");
    assertEquals(businessClass.concat("Java"), "Java");
  }

  @Test
  public void testInTestNG2() {
    System.out.println("Second test");
    assertEquals(businessClass.concat("Java", "17"), "Java 17");
  }

  @AfterMethod
  public void tearDown() {
    businessClass.stop();
  }
}
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
26
$ mvn test
...
[INFO] --- maven-surefire-plugin:3.0.0-M5:test (default-test) @ maven-surefire ---
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running TestSuite
Starting....
First test
Stopping....
Starting....
Second test
Stopping....
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.355 s - in TestSuite
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.962 s
[INFO] Finished at: 2021-11-29T16:02:01+01:00
[INFO] ------------------------------------------------------------------------

Wszystko zadziałało identycznie jak w przypadku rozwiązania POJO. Sprawdźmy jeszcze co się stanie, gdy napiszemy test, który nie przechodzi.

1
2
3
4
5
6
7
8
9
10
  ...

  @Test
  public void testInTestNG3() {
    System.out.println("Third test");
    assertEquals(businessClass.concat("Java", "17"), "Java");
  }

  ...
}

W rezultacie otrzymujemy następujące wyniki w terminalu.

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
26
27
28
29
30
31
32
33
34
35
36
37
38
$ mvn test
...
[INFO] --- maven-surefire-plugin:3.0.0-M5:test (default-test) @ maven-surefire ---
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running TestSuite
Starting....
First test
Stopping....
Starting....
Second test
Stopping....
Starting....
Third test
Stopping....
[ERROR] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.416 s <<< FAILURE! - in TestSuite
[ERROR] TestNGTest.testInTestNG3  Time elapsed: 0.005 s  <<< FAILURE!
java.lang.AssertionError: expected [Java] but found [Java 17]
        at TestNGTest.testInTestNG3(TestNGTest.java:31)

[INFO]
[INFO] Results:
[INFO]
[ERROR] Failures:
[ERROR]   TestNGTest.testInTestNG3:31 expected [Java] but found [Java 17]
[INFO]
[ERROR] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.003 s
[INFO] Finished at: 2021-11-29T16:12:15+01:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.0.0-M5:test (default-test) on project maven-surefire: There are test failures.
...

Wszystko jest tak jak w POJO poza jednym szczegółem. TestNG wskazuje nam różnice pomiędzy tym czego oczekiwaliśmy z tym co otrzymaliśmy. Według mnie jest to naprawdę pomocne przy weryfikacji działania naszego kodu. Sprawdźmy na koniec jak sytuacja ma się dla biblioteki JUnit, a dokładniej jej wersji 5.

Testy napisane w JUnit

Dodajemy następną zależność do pom.xml.

1
2
3
4
5
6
7
8
9
10
  ...
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.8.2</version>
    <scope>test</scope>
  </dependency>

  ...
</project>

I znowu, zatrzymajmy się na sekundę. Przejdźmy do terminala i wpiszmy znaną formułkę mvn test.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ mvn test
...
[INFO] --- maven-surefire-plugin:3.0.0-M5:test (default-test) @ maven-surefire ---
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.464 s
[INFO] Finished at: 2021-11-29T17:10:31+01:00
[INFO] ------------------------------------------------------------------------

Jak ręką odjął, nie wykonały się teraz ani testy napisane w POJO, ani te z TestNG! Jak widać największy priorytet ma biblioteka JUnit. Za chwilę przekonamy się jak to naprawić. Jednak najpierw utwórzmy testy w JUnit.

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
26
27
28
29
30
31
32
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class JUnitTest {

  BusinessClass businessClass = new BusinessClass();

  @BeforeEach
  public void setUp() {
    businessClass.run();
  }

  @Test
  public void testInJUnit1() {
    System.out.println("First test");
    assertEquals("Java", businessClass.concat("Java"));
  }

  @Test
  public void testInJUnit2() {
    System.out.println("Second test");
    assertEquals("Java 17", businessClass.concat("Java", "17"));
  }

  @AfterEach
  public void tearDown() {
    businessClass.stop();
  }
}

Tutaj małe wtrącenie. Jak widzisz w JUnit dla assertEquals na początku podajemy wartość oczekiwaną, a dopiero jako drugi parametr przekazujemy uzyskany wynik z metody. Natomiast w TestNG jest odwrotnie! Łatwo o pomyłkę. Z tego powodu polecam pisać asercje przy użyciu AssertJ, który charakteryzuje się fluent API. Zachęcam do zajrzenia tutaj, aby zapoznać się z kilkoma przykładami jego użycia.

Dobra, wróćmy do Surefire i użycia JUnit. Wpisujemy ponownie w konsolę komendę mvn test.

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
26
$ mvn test
...
[INFO] --- maven-surefire-plugin:3.0.0-M5:test (default-test) @ maven-surefire ---
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running JUnitTest
Starting....
First test
Stopping....
Starting....
Second test
Stopping....
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.052 s - in JUnitTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.878 s
[INFO] Finished at: 2021-11-30T11:10:12+01:00
[INFO] ------------------------------------------------------------------------

Wyniki są identyczne jak w poprzednich przykładach. Na koniec dodajmy nieprzechodzący test i uruchommy Mavena.

1
2
3
4
5
6
7
8
9
10
  ...

  @Test
  public void testInJUnit3() {
    System.out.println("Third test");
    assertEquals("Java", businessClass.concat("Java", "17"));
  }

  ...
}
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
26
27
28
29
30
31
32
33
34
35
36
37
38
$ mvn test
...
[INFO] --- maven-surefire-plugin:3.0.0-M5:test (default-test) @ maven-surefire ---
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running JUnitTest
Starting....
First test
Stopping....
Starting....
Second test
Stopping....
Starting....
Third test
Stopping....
[ERROR] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.056 s <<< FAILURE! - in JUnitTest
[ERROR] JUnitTest.testInJUnit3  Time elapsed: 0.003 s  <<< FAILURE!
org.opentest4j.AssertionFailedError: expected: <Java> but was: <Java 17>
        at JUnitTest.testInJUnit3(JUnitTest.java:31)

[INFO]
[INFO] Results:
[INFO]
[ERROR] Failures:
[ERROR]   JUnitTest.testInJUnit3:31 expected: <Java> but was: <Java 17>
[INFO]
[ERROR] Tests run: 3, Failures: 1, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.735 s
[INFO] Finished at: 2021-11-30T11:28:26+01:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.0.0-M5:test (default-test) on project maven-surefire: There are test failures.
...

W tym przypadku również mamy podgląd na różnice pomiędzy wynikiem oczekiwanym a uzyskanym. Wydaje mi się, że już wyklarował się obraz w jaki sposób możemy korzystać z Surefire dla różnych bibliotek testowych. Pozostaje jeszcze kwestia mieszania się ich, czego konsekwencją jest wykonywanie tylko niektórych testów.

Jak sprawić, aby TestNG i JUnit wykonywały się razem?

Na to pytanie musiałem znaleźć odpowiedź pytając wyszukiwarkę Google. Było to nie lada wyzwanie, ponieważ podpowiedzi z StackOverflow nie chciały działać. Dopiero artykuł Andrey Redko na JavaCodeGeeks rozwiązał sprawę. Według niego na początku musimy dodać odpowiednie zależności do naszego pluginu (czyli to co pisali twórcy o nieświadomości Surefire o bibliotekach przestaje być prawdą).

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
26
27
28
29
  ...
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.0.0-M5</version>
        <configuration>
          <includes>
            <include>**/*Spec.*</include>
            <include>**/*Test.*</include>
          </includes>
        </configuration>
        <dependencies>
          <dependency>
            <groupId>org.apache.maven.surefire</groupId>
            <artifactId>surefire-junit-platform</artifactId>
            <version>3.0.0-M5</version>
          </dependency>
          <dependency>
            <groupId>org.apache.maven.surefire</groupId>
            <artifactId>surefire-testng</artifactId>
            <version>3.0.0-M5</version>
          </dependency>
        </dependencies>
      </plugin>
    </plugins>
  </build>
</project>

Czy to rozwiązuje problem? Sprawdźmy! Wpisujemy mvn test w konsolę i otrzymujemy komunikat - “org.junit.platform.commons.PreconditionViolationException: Cannot create Launcher without at least one TestEngine; consider adding an engine implementation JAR to the classpath”. Niezbędne jest, więc dostarczenie silnika testowego. Powiedzmy, że zdecydowaliśmy się na silnik JUnit, więc musimy zmienić wcześniejszą zależność junit-jupiter-api na junit-jupiter-engine (on zawiera w sobie junit-jupiter-api, nie trzeba duplikować zależności). Teraz odpalając mvn test wszystko zadziała zgodnie z oczekiwaniami. Dla czytelności dodałem w komunikatach testów informację o tym, z której biblioteki korzystają.

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
$ mvn test
...
[INFO] --- maven-surefire-plugin:3.0.0-M5:test (default-test) @ maven-surefire ---
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running JUnitTest
Starting....
First test in JUnit
Stopping....
Starting....
Second test in JUnit
Stopping....
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.054 s - in JUnitTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running TestSuite
Starting....
First test in TestNG
Stopping....
Starting....
Second test in TestNG
Stopping....
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.367 s - in TestSuite
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  4.058 s
[INFO] Finished at: 2021-11-30T12:37:32+01:00
[INFO] ------------------------------------------------------------------------

Czy jest lepsze rozwiązanie?

Pewnie, że tak! We wcześniejszym rozwiązaniu jest pewien mankament. Dołączając zależność TestNG do Surefire od razu generowały nam się raporty w postaci strony HTML. Co więcej nie były w nim uwzględnione testy napisane w JUnit. Próbowałem nad tym zapanować, ale poległem. Nie udało mi się znaleźć rozwiązania tego problemu (dołączyć JUnit do raportu czy wyłączyć całkowicie raporty HTML). Jak można sobie w takim razie z tym poradzić?

Rozwiązaniem znajduje się na stronie Mavena, a dokładnie pod nagłówkiem “How to run TestNG tests within Jupiter engine”. Musimy zmienić POM na następujący.

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
  ...

  <dependencies>
    <dependency>
      <groupId>org.testng</groupId>
      <artifactId>testng</artifactId>
      <version>7.4.0</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <version>5.8.2</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>com.github.testng-team</groupId>
      <artifactId>testng-junit5</artifactId>
      <version>0.0.1</version>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.platform</groupId>
          <artifactId>junit-platform-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.0.0-M5</version>
        <configuration>
          <includes>
            <include>**/*Spec.*</include>
            <include>**/*Test.*</include>
          </includes>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Wracamy, więc z junit-jupiter-engine do junit-jupiter-api oraz usuwamy zależności z pluginu Surefire. Dodatkowo trzeba dodać nową zależność testng-junit5 wyłączając z niego junit-platform-engine. Teraz wywołując mvn test wszystko działa jak wcześniej, ale w katalogu target/surefire-reports nie ma już żadnych plików HTML. Właśnie o to nam chodziło!

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
$ mvn test
...
[INFO] --- maven-surefire-plugin:3.0.0-M5:test (default-test) @ maven-surefire ---
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running TestNGTest
Starting....
First test in TestNG
Stopping....
Starting....
Second test in TestNG
Stopping....

===============================================
Command line suite
Total tests run: 2, Passes: 2, Failures: 0, Skips: 0
===============================================

[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.271 s - in TestNGTest
[INFO] Running JUnitTest
Starting....
First test in JUnit
Stopping....
Starting....
Second test in JUnit
Stopping....
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.036 s - in JUnitTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.707 s
[INFO] Finished at: 2021-11-30T14:28:16+01:00
[INFO] ------------------------------------------------------------------------

WAŻNE! Maven informuje, że nie bierze żadnej odpowiedzialności za błędy, które powstały przez zależność do com.github.testng-team:testng-junit5 (twórcy tego narzędzia polecają przejście na https://github.com/junit-team/testng-engine, czyli zamianę zależności com.github.testng-team:testng-junit5 na org.junit.support:testng-engine).

1
2
3
4
5
6
7
8
9
10
  ...
  <dependency>
    <groupId>org.junit.support</groupId>
    <artifactId>testng-engine</artifactId>
    <version>1.0.1</version>
    <scope>test</scope>
  </dependency>
</dependencies>

...

A co z raportami HTML z testów?

Jeśli chcielibyśmy generować raporty testów w postaci HTML to konieczne będzie dodanie kolejnego pluginu - maven-site-plugin. Pozwala nam on wygenerować stronę naszego projektu, gdzie znajdziemy wszystkie informacje o nim. Oczywiście będzie też tam załączony raport z testów o ile dodamy odpowiedni plugin do sekcji project.reporting.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
      ...
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-site-plugin</artifactId>
        <version>3.9.1</version>
      </plugin>
    </plugins>
  </build>

  <reporting>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-report-plugin</artifactId>
        <version>3.0.0-M5</version>
      </plugin>
    </plugins>
  </reporting>
</project>

Teraz wywołując odpowiednią komendę Mavena wygenerowany zostanie plik index.html w katalogu target/site. Tym poleceniem jest mvn site, czyli inny lifecycle niż domyślny. Gdy już je wywołamy otwórzmy sobie naszą stronę. W niej znajdziemy zakładkę “Project Reports”, a po jej wybraniu ukaże się nam “Surefire Report”. Przechodząc do niej dostaniemy pełen raport wykonania naszych testów jednostkowych.

Raport z testów jednostkowych w formie strony HTML
Raport z testów jednostkowych w formie strony HTML

Strona zachowana jest w stylu retro, ale przynajmniej nie musimy nic pisać w HTML czy CSS. Cały raport dostajemy za darmo wywołując odpowiednią komendę.

Podsumowanie

W tym artykule zobaczyliśmy w jaki sposób możemy wywoływać testy jednostkowe z poziomu Mavena. Sprawdziliśmy również jakie problemy możemy napotkać podczas korzystania z Surefire. Na koniec wygenerowaliśmy raport w formie graficznej, który możemy przedstawić osobie nietechnicznej (o ile odpowiednio ponazywaliśmy metody testowe). Mam nadzieję, że dzisiejszy wpis okaże się dla Ciebie pomocny i zachęci Cię do automatyzowania swoich projektów!