W jednym z poprzednich wpisów poruszyliśmy temat pluginu Surefire służącego do uruchamiania testów jednostkowych w Maven. Dzisiaj natomiast skupimy się na kolejnym rozszerzeniu, a mianowicie na Failsafe. Powodem jego powstania była chęć uruchamiania testów integracyjnych przy wykorzystaniu jednej komendy. Widać w takim razie, że obydwa pluginy mają ze sobą wiele wspólnego. Ich celem jest ułatwienie testowania naszego oprogramowania. Przejdźmy zatem do sedna i zobaczmy jak wygląda konfiguracja Failsafe.

Ogólny pogląd na Failsafe

Maven posiada aż cztery fazy odpowiedzialne za obsługę testów integracyjnych:

  • pre-integration-test - przygotowanie środowiska do testów integracyjnych
  • integration-test - uruchamianie testów integracyjnych
  • post-integration-test - zamknięcie środowiska do testów integracyjnych
  • verify - sprawdzenie wyników wykonania testów integracyjnych

Gdybyśmy używali Surefire do uruchamiania testów integracyjnych to w pewnym momencie napotkalibyśmy poważny problem. W przypadku nieprzejścia jednego z testów podczas budowania aplikacji cały proces natychmiast by się zakończył na fazie integration-test. Wtedy nie uruchomiłaby się krok odpowiedzialny za posprzątanie po testach integracyjnych. Mogłoby to skutkować naprawdę nieprzewidywalnymi rezultatami. Co odróżnia, więc Failsafe od Surefire w tej sytuacji? To, że nie zakończy się on od razu po znalezieniu błędu w testach tylko wykona jeszcze fazę post-integration-test, aby zamknąć prawidłowo środowisko testowe.

Failsafe jest używany do uruchomienia testów integracyjnych podczas dwóch faz: integration-test oraz verify. Jak dobrze pamiętasz z poprzedniego artykułu nie każdy phase powinien być uruchamiany z wiersza poleceń. Z tego powodu miej na uwadze, żeby nie używać mvn integration-test tylko mvn verify. Jest jeden konkretny powód stojący za tym - jeśli uruchomimy bezpośrednio fazę integration-test to, tak jak w przypadku Surefire, nie wykona się post-integration-test.

Po wykonaniu testów integracyjnych chcielibyśmy mieć pogląd na rezultat ich uruchomienia. Failsafe generuje nam raporty w dwóch formatach: TXT oraz XML, które zostają domyślnie umieszczone w katalogu ${basedir}/target/failsafe-reports/. Tak jak w przypadku Surefire tutaj również możemy uzyskać formę graficzną raportu. Dzieje się to dzięki wykorzystaniu Maven Surefire Report Plugin, który tworzy wynikowy plik w formacie HTML.

Wykorzystanie pluginu Failsafe

Nie będę w tym miejscu przytaczał wszystkich bibliotek do testowania kodu, ponieważ uruchamianie testów odbywa się w ten sam sposób co w przypadku Surefire. Zamiast tego skupimy się na tym w jaki sposób w ogóle uruchomić testy integracyjne z wykorzystaniem pluginu Failsafe. Na początku należy stworzyć przykładowy projekt. Wykorzystamy do tego oczywiście Springa, w którym stworzymy kontroler do komunikacji HTTP. W ten sposób będziemy mogli sprawdzić poprzez test integracyjny czy uzyskamy połączenie z naszą aplikacją.

Tworzymy projekt w Spring Boot

Po otworzeniu projektu w IDE usuwamy od razu obecną w niej klasę testową MavenFailsafeApplicationTests. Nie jest ona nam niezbędna, a tylko zaburzy pogląd na działanie pluginu. Przechodzimy, więc do napisania przykładowego kodu.

1
2
3
4
5
6
7
8
9
10
11
12
13
package pl.devcezz.mavenfailsafe;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class HelloController {

  @GetMapping("/hello")
  String hello() {
    return "Nice to see you again!";
  }
}

Dodajemy najprostszy punkt wejścia do naszej aplikacji, czyli metodę GET na ścieżce /hello zwracającą literał Nice to see you again!. Dobra, to bierzemy się teraz za napisanie testu integracyjnego. Wykorzystamy do tego klasę URLConnection.

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
package pl.devcezz.mavenfailsafe;

import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Scanner;

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

class HelloControllerTest {

  @Test
  void should_get_welcome_response() throws IOException {
    String url = "http://localhost:8080/hello";
    URLConnection connection = new URL(url).openConnection();
    String responseBody;
    try (InputStream response = connection.getInputStream(); Scanner scanner = new Scanner(response)) {
      responseBody = scanner.nextLine();
    }
    assertEquals("Nice to see you again!", responseBody);
  }
}

Podajemy URL naszego endpointu, otwieramy połączenie, pobieramy zawartość i sprawdzamy czy równa się ona spodziewanemu rezultatowi. Test został napisany z wykorzystaniem JUnit 5, ponieważ jest to domyślna biblioteka dołączana do zależności spring-boot-maven-plugin.

Jeśli chcielibyśmy uruchomić po prostu test bez przygotowania w naszym IDE to dostaniemy komunikat java.net.ConnectException: Connection refused: connect. Jeżeli na początku uruchomimy naszą aplikację opartą o Spring Boot i odpalimy test to pasek od razu przechodzi na zielono. Jednak jaką mamy tutaj automatyzację procesu? Spróbujmy pokombinować z Mavenem uruchamianym z wiersza poleceń.

Odpalamy weryfikację projektu Maven w konsoli

Pamiętajmy, aby na początku zamknąć naszą aplikację. Teraz wpiszmy do konsoli polecenie mvn clean verify. Lepiej dawać clean, ponieważ wyczyścimy wtedy wcześniej skompilowany kod, który może być już nieaktualny.

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
$ mvn clean verify
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< pl.devcezz:maven-failsafe >----------------------
[INFO] Building maven-failsafe 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
...
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ maven-failsafe ---
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running pl.devcezz.mavenfailsafe.HelloControllerTest
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.059 s <<< FAILURE! - in pl.devcezz.mavenfailsafe.HelloControllerTest
[ERROR] should_get_welcome_response  Time elapsed: 0.053 s  <<< ERROR!
java.net.ConnectException: Connection refused: connect
    at pl.devcezz.mavenfailsafe.HelloControllerTest.should_get_welcome_response(HelloControllerTest.java:20)

[INFO]
[INFO] Results:
[INFO]
[ERROR] Errors:
[ERROR]   HelloControllerTest.should_get_welcome_response:20 ╗ Connect Connection refuse...
[INFO]
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  4.347 s
[INFO] Finished at: 2021-12-16T14:22:07+01:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.22.2:test (default-test) on project maven-failsafe: There are test failures.
[ERROR]
[ERROR] Please refer to D:\git\maven-failsafe\target\surefire-reports for the individual test results.
[ERROR] Please refer to dump files (if any exist) [date].dump, [date]-jvmRun[N].dump and [date].dumpstream.
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException

Wprawne oko zauważy, że uruchomił nam się plugin Surefire zamiast Failsafe z takim samym rezultatem jak w IDE. Oczywiście jeśli aplikacja nadal stałaby na środowisku lokalnym na porcie 8080 (domyślny port uruchomienia aplikacji wykorzystującej Spring Boot) to testy byłby zielony. Nie dziwi w sumie sytuacja, że Failsafe nie zadziałał. Nie dodaliśmy go przecież do pluginów w POM. Dlaczego natomiast Surefire się odpalił?

Zaglądając do pliku POM widzimy tam sekcję parent, która odnosi się do spring-boot-starter-parent (przykład dziedziczenia). W nim jest ta sama sytuacja, czyli istnieje odwołanie do spring-boot-dependencies. Dopiero w tym pliku POM jeśli wyszukamy frazę surefire to znajdziemy Surefire w sekcji pluginów. Okazuje się, że Failsafe także jest tutaj zdefiniowany, co więcej znajduje się również w spring-boot-starter-parent.

Przyglądając się dłużej spostrzeżemy, że definicje tych pluginów są w sekcji pluginManagement. Oznacza to, że jeśli jawnie nie zadeklarujemy w naszym projekcie, że chcemy skorzystać z danego pluginu rodzica to nie zostanie on po prostu użyty. No dobrze, ale dalej nie wiemy dlaczego Surefire pojawił się w konsoli przy wywołaniu mvn clean verify.

Dopiero spojrzenie w kod Mavena da nam odpowiedź na to pytanie. Domyślnie Maven pakuje projekty w pliki JAR. Okazuje się, że dla tego sposobu tworzenia artefaktu mamy domyślnie do dyspozycji aż 8 pluginów: clean, compiler, deploy, install, jar, resources, site i właśnie surefire. Jako eksperyment zmieńmy element packaging przekazując mu wartość pom. Po uruchomieniu mvn clean verify nie powinien nam się już pokazać w konsoli surefire. Wróćmy jednak do domyślnego pakowania aplikacji.

To jak włączyć Failsafe?

Po prostu musimy dodać w pom.xml do sekcji project.build.plugins nowy plugin. Nie trzeba określać wersji, ponieważ z racji dziedziczenia otrzymamy konfigurację dostępną w rodzicu POM Springa.

1
2
3
4
5
6
7
8
      ...
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-failsafe-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

Jeśli teraz zakomentujemy nasz test i uruchomimy w konsoli mvn clean verify to w końcu dostaniemy informację, że Failsafe się odpalił, a dokładniej jego dwa goals: integration-test i verify.

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
$ mvn clean verify
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< pl.devcezz:maven-failsafe >----------------------
[INFO] Building maven-failsafe 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
...
[INFO]
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ maven-failsafe ---
[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] --- maven-jar-plugin:3.2.0:jar (default-jar) @ maven-failsafe ---
[INFO] Building jar: D:\git\maven-failsafe\target\maven-failsafe-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- spring-boot-maven-plugin:2.6.1:repackage (repackage) @ maven-failsafe ---
[INFO] Replacing main artifact with repackaged archive
[INFO]
[INFO] --- maven-failsafe-plugin:2.22.2:integration-test (default) @ maven-failsafe ---
[INFO]
[INFO] --- maven-failsafe-plugin:2.22.2:verify (default) @ maven-failsafe ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  5.124 s
[INFO] Finished at: 2021-12-16T15:45:05+01:00
[INFO] ------------------------------------------------------------------------

Natomiast jeśli ponownie odkomentujemy nasz test to oczywiście nie przejdzie on z wcześniej wymienionych powodów. Jednak dalej jest on uruchamiany przez Surefire zamiast Failsafe. Pytanie, więc brzmi - dlaczego? Jeśli przyjrzymy się domyślnym ustawieniom obydwu pluginów zobaczymy, że Surefire wyszukuje te testy, które pasują do następujących wzorów: **/Test*.java, **/*Test.java, **/*Tests.java i **/*TestCase.java. Natomiast Failsafe poszukuje klas pasujących do masek: **/IT*.java, **/*IT.java oraz **/*ITCase.java. Wychodzi na to, że zmieniając nazwę HelloControllerTest na HelloControllerIT to Failsafe zaopiekuje się naszym testem. Oczywiście te wzory w obydwu pluginów można dostosowywać do własnych potrzeb.

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
48
49
50
51
52
53
54
55
$ mvn clean verify
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< pl.devcezz:maven-failsafe >----------------------
[INFO] Building maven-failsafe 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
...
[INFO]
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ maven-failsafe ---
[INFO]
[INFO] --- maven-jar-plugin:3.2.0:jar (default-jar) @ maven-failsafe ---
[INFO] Building jar: D:\git\maven-failsafe\target\maven-failsafe-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- spring-boot-maven-plugin:2.6.1:repackage (repackage) @ maven-failsafe ---
[INFO] Replacing main artifact with repackaged archive
[INFO]
[INFO] --- maven-failsafe-plugin:2.22.2:integration-test (default) @ maven-failsafe ---
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running pl.devcezz.mavenfailsafe.HelloControllerIT
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.059 s <<< FAILURE! - in pl.devcezz.mavenfailsafe.HelloControllerIT
[ERROR] should_get_welcome_response  Time elapsed: 0.05 s  <<< ERROR!
java.net.ConnectException: Connection refused: connect
    at pl.devcezz.mavenfailsafe.HelloControllerIT.should_get_welcome_response(HelloControllerIT.java:20)

[INFO]
[INFO] Results:
[INFO]
[ERROR] Errors:
[ERROR]   HelloControllerIT.should_get_welcome_response:20 ╗ Connect Connection refused:...
[INFO]
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0
[INFO]
[INFO]
[INFO] --- maven-failsafe-plugin:2.22.2:verify (default) @ maven-failsafe ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  5.505 s
[INFO] Finished at: 2021-12-16T16:09:39+01:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-failsafe-plugin:2.22.2:verify (default) on project maven-failsafe: There are test failures.
[ERROR]
[ERROR] Please refer to D:\git\maven-failsafe\target\failsafe-reports for the individual test results.
[ERROR] Please refer to dump files (if any exist) [date].dump, [date]-jvmRun[N].dump and [date].dumpstream.
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException

Stało się tak jak oczekiwaliśmy, Failsafe w końcu nam zadziałał! Pora teraz zautomatyzować uruchamianie naszej aplikacji przed wykonaniem testów integracyjnych i wyłączanie jej po ukończonym zadaniu.

Uruchamianie aplikacji na potrzeby testów integracyjnych

Zastanówmy się co musimy zrobić, aby osiągnąć nasz cel. Wiemy, że w domyślnym lifecycle Mavena znajdują się phases pre-integration-test oraz post-integration-test. Jest to idealne miejsce na doczepienie uruchomienia naszej aplikacji i wyłączenia jej. Na pomoc przychodzi nam już istniejący w POM plugin spring-boot-maven-plugin, który został stworzony do takich celów. W jego konfiguracji musimy dodać do węzła executions odpowiedni 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
  ...
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <executions>
          <execution>
            <id>pre-integration-test</id>
            <goals>
              <goal>start</goal>
            </goals>
          </execution>
          <execution>
            <id>post-integration-test</id>
            <goals>
              <goal>stop</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      ...
    </plugins>
  </build>
</project>

Do phase pre-integration-test podpinamy goal start, natomiast dla post-integration-test dodajemy stop zatrzymujący aplikację Spring Boota. Teraz wywołując mvn clean verify powinniśmy zobaczyć, że test przejdzie pod wodzą Failsafe.

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
$ mvn clean verify
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< pl.devcezz:maven-failsafe >----------------------
[INFO] Building maven-failsafe 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
....
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ maven-failsafe ---
[INFO]
[INFO] --- maven-jar-plugin:3.2.0:jar (default-jar) @ maven-failsafe ---
[INFO] Building jar: D:\git\maven-failsafe\target\maven-failsafe-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- spring-boot-maven-plugin:2.6.1:repackage (repackage) @ maven-failsafe ---
[INFO] Replacing main artifact with repackaged archive
[INFO]
[INFO] --- spring-boot-maven-plugin:2.6.1:start (pre-integration-test) @ maven-failsafe ---
[INFO] Attaching agents: []

  .   ____      _      __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.6.1)

2021-12-16 16:12:07.764  INFO 29312 --- [       main] p.d.m.MavenFailsafeApplication       : Starting MavenFailsafeApplication using Java 16.0.1 on DESKTOP-4EG7G8N with PID 29312 (D:\git\maven-failsafe\target\classes started by sanec in D:\git\maven-failsafe)
2021-12-16 16:12:07.766  INFO 29312 --- [       main] p.d.m.MavenFailsafeApplication       : No active profile set, falling back to default profiles: default
2021-12-16 16:12:08.972  INFO 29312 --- [       main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2021-12-16 16:12:08.988  INFO 29312 --- [       main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2021-12-16 16:12:08.989  INFO 29312 --- [       main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.55]
2021-12-16 16:12:09.114  INFO 29312 --- [       main] o.a.c.c.C.[Tomcat].[localhost].[/]     : Initializing Spring embedded WebApplicationContext
2021-12-16 16:12:09.115  INFO 29312 --- [       main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1290 ms
2021-12-16 16:12:09.620  INFO 29312 --- [       main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2021-12-16 16:12:09.640  INFO 29312 --- [       main] p.d.m.MavenFailsafeApplication       : Started MavenFailsafeApplication in 2.3 seconds (JVM running for 2.873)
[INFO]
[INFO] --- maven-failsafe-plugin:2.22.2:integration-test (default) @ maven-failsafe ---
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running pl.devcezz.mavenfailsafe.HelloControllerIT
2021-12-16 16:12:11.039  INFO 29312 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]     : Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-12-16 16:12:11.039  INFO 29312 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet    : Initializing Servlet 'dispatcherServlet'
2021-12-16 16:12:11.040  INFO 29312 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet    : Completed initialization in 1 ms
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.13 s - in pl.devcezz.mavenfailsafe.HelloControllerIT
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] --- spring-boot-maven-plugin:2.6.1:stop (post-integration-test) @ maven-failsafe ---
[INFO] Stopping application...
2021-12-16 16:12:11.652  INFO 29312 --- [on(4)-127.0.0.1] inMXBeanRegistrar$SpringApplicationAdmin : Application shutdown requested.
[INFO]
[INFO] --- maven-failsafe-plugin:2.22.2:verify (default) @ maven-failsafe ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  8.854 s
[INFO] Finished at: 2021-12-16T16:12:11+01:00
[INFO] ------------------------------------------------------------------------

Oczywiście w przypadku aplikacji, które nie korzystają ze Springa trzeba będzie również podpiąć się pod te konkretne fazy. Na przykład dla Jetty istnieje plugin jetty-maven-plugin, który również ma goals start oraz stop dla tego typu przypadków.

Kategoryzowanie testów

Profile i maski

Załóżmy, że mamy testy integracyjne, które są dosyć szybkie i chcemy je puszczać raz na jakiś czas oraz takie, których czas trwania przekracza kilka minut. Wtedy chcielibyśmy, aby były one uruchamiane każdej nocy, gdy nikomu nie będzie to przeszkadzało. Musimy, więc jakoś rozwiązać ten problem. Tutaj na pomoc przychodzą nam profile Mavena, w których odpowiednio skonfigurujemy plugin Failsafe. Na start trzeba dodać, że w tym przypadku należy odpowiednio nazywać klasy testowe, ponieważ to po nich będziemy kategoryzować czy test trwa długo czy nie. Utwórzmy, więc do istniejących testów nową klasę testową.

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
package pl.devcezz.mavenfailsafe;

import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Scanner;

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

class HelloControllerSlowIT {

  @Test
  void should_get_welcome_response_after_some_time() throws IOException, InterruptedException {
    String url = "http://localhost:8080/hello";
    URLConnection connection = new URL(url).openConnection();
    String responseBody;
    Thread.sleep(3_000);
    try (InputStream response = connection.getInputStream(); Scanner scanner = new Scanner(response)) {
      responseBody = scanner.nextLine();
    }
    assertEquals("Nice to see you again!", responseBody);
  }
}

Jest to kopiowa wcześniej napisanego testu. Jednak tutaj dołożyliśmy jeszcze Thread.sleep(3000), aby zajmował on trochę więcej czasu. Na co na prawdę warto zwrócić uwagę to, że nazwa klasy testowej kończy się *SlowIT. Tym sufiksem będziemy zaznaczać klasy, które posiadają długo wykonujące się testy. Przejdźmy zatem do sedna, czyli pliku POM. Tak jak napisałem wcześniej musimy dodać element profiles.

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
  ...
  <profiles>
      <profile>
        <id>only-slow-tests</id>
        <build>
          <plugins>
            <plugin>
              <groupId>org.apache.maven.plugins</groupId>
              <artifactId>maven-failsafe-plugin</artifactId>
              <configuration>
                <includes>
                  <include>**/*SlowIT.java</include>
                </includes>
              </configuration>
            </plugin>
          </plugins>
        </build>
      </profile>
      <profile>
        <id>exclude-slow-tests</id>
        <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-failsafe-plugin</artifactId>
            <configuration>
              <excludes>
                <exclude>**/*SlowIT.java</exclude>
              </excludes>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>
</project>

To co tutaj się zadziało to utworzenie dwóch profili: only-slow-tests oraz exclude-slow-tests. W pierwszym z nich nadpisaliśmy domyślną konfigurację includes Failsafe, aby brał tylko pod uwagę testy kończące się na *SlowIT.java. Natomiast dla exclude-slow-tests nadal będzie obowiązywała standardowa maska, **/IT*.java, **/*IT.java oraz **/*ITCase.java, tylko musimy z niej wykluczyć właśnie **/*SlowIT.java, ponieważ pasuje on do wcześniej wymienionych.

Teraz uruchamiając mvn clean verify uruchomią się wszystkie testy, a dla mvn clean verify -Ponly-slow-tests tylko te określone jako wolne (dzięki -P wskazujemy profil z jakim uruchomić się mają komendy przekazane do Maven). Natomiast wpisując mvn clean verify -Pexclude-slow-tests odpalą się wyłącznie mniej uciążliwe testy.

Sposób JUnit

Jeśli do pisania testów wykorzystamy JUnit w wersji co najmniej 4.8 to będziemy mogli skorzystać z pojęcia kategorii (brakuje ich natomiast w JUnit 5), które oznaczane są za pomocą adnotacji @Category. Plugin Failsafe wspiera to rozwiązanie przez użycie parametru konfiguracyjnego groups. Na początku stwórzmy trzy interfejsy.

1
2
3
interface SlowTest {}
interface ReallySlowTest extends SlowTest {}
interface FastTest {}

Teraz przenieśmy nasze obecne testy do jednej klasy i dodajmy jeszcze jeden dodatkowy. Oznaczmy odpowiednie metody testowe przez adnotacje @Category przekazując do niej odpowiedni argument w postaci wcześniej zdefiniowanych interfejsów. Oczywiście nazwa klasy musi odpowiadać domyślnym wzorcom Failsafe.

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
48
49
50
51
52
53
package pl.devcezz.mavenfailsafe;

import org.junit.Test;
import org.junit.experimental.categories.Category;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.Scanner;

import static org.junit.Assert.assertEquals;

public class HelloControllerIT {

  @Test
  @Category(pl.devcezz.mavenfailsafe.FastTest.class)
  public void should_get_welcome_response() throws IOException {
    String url = "http://localhost:8080/hello";
    URLConnection connection = new URL(url).openConnection();
    String responseBody;
    try (InputStream response = connection.getInputStream(); Scanner scanner = new Scanner(response)) {
      responseBody = scanner.nextLine();
    }
    assertEquals("Nice to see you again!", responseBody);
  }

  @Test
  @Category(pl.devcezz.mavenfailsafe.SlowTest.class)
  public void should_get_welcome_response_after_some_time() throws IOException, InterruptedException {
    String url = "http://localhost:8080/hello";
    URLConnection connection = new URL(url).openConnection();
    String responseBody;
    Thread.sleep(3_000);
    try (InputStream response = connection.getInputStream(); Scanner scanner = new Scanner(response)) {
      responseBody = scanner.nextLine();
    }
    assertEquals("Nice to see you again!", responseBody);
  }

  @Test
  @Category(pl.devcezz.mavenfailsafe.ReallySlowTest.class)
  public void should_get_welcome_response_after_really_long_time() throws IOException, InterruptedException {
    String url = "http://localhost:8080/hello";
    URLConnection connection = new URL(url).openConnection();
    String responseBody;
    Thread.sleep(10_000);
    try (InputStream response = connection.getInputStream(); Scanner scanner = new Scanner(response)) {
      responseBody = scanner.nextLine();
    }
    assertEquals("Nice to see you again!", responseBody);
  }
}

UWAGA! W Junit 4 wszystkie klasy testowe oraz metody muszą być publiczne, aby były widziane przez Mavena.

Oczywiście te adnotacje można umieszczać również nad klasami. Na sam koniec musimy skonfigurować Failsafe i będziemy gotowi do uruchomienia Mavena. Usuniemy wcześniej zdefiniowane profile i dodamy nowe. Dodatkowo trzeba wykluczyć JUnit 5 z zależności testowej Spring Boota oraz dorzucić JUnit 4.

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
  ...
  <properties>
    <java.version>11</java.version>
    <testcase.groups/>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.jupiter</groupId>
          <artifactId>junit-jupiter</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13.2</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <executions>
          <execution>
            <id>pre-integration-test</id>
            <goals>
              <goal>start</goal>
            </goals>
          </execution>
          <execution>
            <id>post-integration-test</id>
            <goals>
              <goal>stop</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-failsafe-plugin</artifactId>
        <version>3.0.0-M5</version>
        <configuration>
          <groups>${testcase.groups}</groups>
        </configuration>
      </plugin>
    </plugins>
  </build>

  <profiles>
    <profile>
      <id>only-slow-tests</id>
      <properties>
        <testcase.groups>pl.devcezz.mavenfailsafe.SlowTest</testcase.groups>
      </properties>
    </profile>
    <profile>
      <id>only-fast-tests</id>
      <properties>
        <testcase.groups>pl.devcezz.mavenfailsafe.FastTest</testcase.groups>
      </properties>
    </profile>
  </profiles>
</project>

Zamiast konfigurować osobno każdy z profili wykorzystaliśmy nadpisanie właściwości o nazwie testcase.groups. Domyślnie jest on pusty przez co mvn clean verify uruchomi wszystkie testy. W celu korzystania z kategorii JUnit musimy w konfiguracji pluginu Failsafe ustawić element groups wskazujący na pełną nazwę danego interfejsu. Jeśli ustawimy wskazanie na SlowTest, po którym dziedziczy ReallySlowTest to testy dla tych dwóch kategorii wykonają się. W ten sposób dostaliśmy mechanizm pozwalający na wybieranie metod testowych, a nie tylko całych klas jako tych, które możemy segregować. Jednak uzależniamy się mocno od jednej biblioteki testowej co nie ma miejsca w przypadku wcześniejszego sposobu.

Uruchamianie pojedynczych testów

W Failsafe jak i Surefire istnieje możliwość uruchomienia wybranej klasy testowej lub metody. Wystarczy wykorzystać parametr it.test (dla Surefire jest to po prostu test) i przekazać do niego nazwę klasy oraz opcjonalnie metody. Powróćmy do wcześniejszego podziału na HelloControllerIT oraz HelloControllerSlowIT, które mają po jednej metodzie testowej. Sprawdźmy kilka przypadków wywołania.

  • mvn -Dit.test=pl.devcezz.mavenfailsafe.HelloControllerIT clean verify - uruchomi tylko testy znajdujące się w HelloControllerIT
  • mvn -Dit.test=pl.devcezz.mavenfailsafe.HelloController*IT clean verify - odpali testy znajdujące się w klasach zaczynających swoją nazwę od pl.devcezz.mavenfailsafe.HelloControllerIT, a kończących się na IT (może być puste albo mające w tym miejscu słowo Slow)
  • mvn -Dit.test=pl.devcezz.mavenfailsafe.HelloControllerIT,pl.devcezz.mavenfailsafe.HelloControllerSlowIT clean verify - uruchomi testy z klas HelloControllerIT oraz HelloControllerSlowIT
  • mvn -Dit.test=pl.devcezz.mavenfailsafe.HelloControllerIT#should_get_welcome_response clean verify - opadli się metoda testowa should_get_welcome_response znajdująca się w klasie HelloControllerIT
  • mvn -Dit.test=pl.devcezz.mavenfailsafe.HelloController*IT#should_get_welcome_response* clean verify - odpali metody testowe zaczynające się od should_get_welcome_response w klasach mających prefiks w postaci pl.devcezz.mavenfailsafe.HelloControllerIT, a kończących się na IT (może być puste albo mające w tym miejscu słowo Slow)

Jak widać dowolność jest ogromna i to nie są wszystkie możliwości pisania wzorców. Po więcej zapraszam pod ten adres.

Pomijanie wykonania testów integracyjnych przy budowaniu aplikacji

Czasami przydatną opcją jest wyłączenie wywołania testów integracyjnych podczas budowania aplikacji. W tej sytuacji należy podać parametr skipITs do komendy mvn clean install. Natomiast jeśli nie chcielibyśmy wywoływać żadnych testów, nawet tych zdefiniowanych w Surefire, to musimy napisać w wierszu poleceń mvn clean install -DskipTests. Istnieje jeszcze możliwość pominięcia kompilowania testów przy pomocy parametru maven.test.skip, którą trzeba ustawić na true. Oddziałuje ona na pluginy Surefire, Failsafe oraz Compiler - mvn clean install -Dmaven.test.skip=true.

Możemy również wyłączyć wykonywanie testów na poziomie pliku POM. Jednak podchodziłbym do tego z dużą ostrożnością, bo może być tak, że zapomnimy o tym i testy nigdy nie będą nam się wykonywać.

1
2
3
4
5
6
7
8
9
10
11
12
      ...
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-failsafe-plugin</artifactId>
        <version>3.0.0-M5</version>
        <configuration>
          <skipITs>true</skipITs>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Debugowanie testów integracyjnych z wykorzystaniem Mavena

W celu weryfikacji działania naszego kodu możemy wykorzystać narzędzie do debugu. W Maven jest ono dostępne dla Failsafe pod parametrem maven.failsafe.debug. Niezbędne nam będzie do tego również środowisko programistyczne. My wykorzystamy IDE Intellij. Wpisujemy komendę mvn -Dmaven.failsafe.debug verify do konsoli i otrzymujemy następującą informację.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ mvn -Dmaven.failsafe.debug verify
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< pl.devcezz:maven-failsafe >----------------------
[INFO] Building maven-failsafe 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
...
[INFO] --- maven-failsafe-plugin:3.0.0-M5:integration-test (default) @ maven-failsafe ---
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Listening for transport dt_socket at address: 5005

Maven zaczął nasłuchiwać na porcie 5005, aby móc się do niego podłączyć zewnętrznym debuggerem. Wchodzimy do IntelliJ otwierając nasz projekt i w konfiguracji uruchamiania wybieramy następującą opcję (wszystkie domyślne wartości powinny być prawidłowe).

Konfiguracja uruchomienia debug w Maven

Klikamy ‘OK’, wybieramy MavenDebug w dostępnych opcjach i klikamy guzik Debug (zielony robak). Oczywiście przed tym trzeba postawić w interesującym nas miejscu Line Breakpoint. Po chwili zostaniemy przeniesieni do debuggera, a dokładniej do naszego wyznaczonego punktu.

Debug podczas uruchomienia Mavena

Podsumowanie

Mam nadzieję, że ten obszerny wpis chociaż trochę przedstawił Ci ideę stojącą za pluginem Failsafe. Widać wiele podobieństw do wcześniej przedstawionego Surefire. Są też miejsca, w których trzeba bardzo uważać, aby nie pogubić się w konfiguracji. Na pewno warto poczytać dokumentację i dobrać odpowiednie rozwiązanie dla siebie.