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 integracyjnychintegration-test
- uruchamianie testów integracyjnychpost-integration-test
- zamknięcie środowiska do testów integracyjnychverify
- 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ą.
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ę wHelloControllerIT
mvn -Dit.test=pl.devcezz.mavenfailsafe.HelloController*IT clean verify
- odpali testy znajdujące się w klasach zaczynających swoją nazwę odpl.devcezz.mavenfailsafe.HelloControllerIT
, a kończących się naIT
(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 klasHelloControllerIT
orazHelloControllerSlowIT
mvn -Dit.test=pl.devcezz.mavenfailsafe.HelloControllerIT#should_get_welcome_response clean verify
- opadli się metoda testowashould_get_welcome_response
znajdująca się w klasieHelloControllerIT
mvn -Dit.test=pl.devcezz.mavenfailsafe.HelloController*IT#should_get_welcome_response* clean verify
- odpali metody testowe zaczynające się odshould_get_welcome_response
w klasach mających prefiks w postacipl.devcezz.mavenfailsafe.HelloControllerIT
, a kończących się naIT
(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).
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.
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.