1. Introduction
Most of the application functionality to date within this course has been formally tested using unit tests and @SpringBootTest
mechanisms executed within Maven Surefire plugin.
This provided us a time-efficient code, test, and debug round trip — with possibly never leaving the IDE.
However, the functionality of the stand-alone application itself has only been demonstrated using ad-hoc manual mechanisms (e.g., curl).
In this lecture, we will look to test a fully assembled application using automated tests and the Maven Failsafe and other plugins.
Although generally slower to execute, Maven integration tests open up opportunities to implement automated tests that require starting and stopping external resources to simulate a more realistic runtime environment. These may be the application server or resources (e.g., database, JMS server) the application will communicate with.
1.1. Goals
You will learn:
-
the need for Maven integration tests
-
how to implement a Maven integration test
-
how to define and activate a reusable Maven integration test setup and execution
1.2. Objectives
At the conclusion of this lecture and related exercises, you will be able to:
-
implement a Maven Failsafe integration test in order to test a Spring Boot executable JAR running as a stand-alone process
-
dynamically generate test instance-specific property values in order to be able to run in a shared CI/CD environment
-
start server process(es) configured with test instance-specific property values
-
execute tests against server process(es)
-
stop server process(es) at the conclusion of a test
-
evaluate test results once server process(es) have been stopped
-
generalize Maven constructs into a conditional Maven parent profile
-
implement and trigger a Maven integration test using a Maven parent profile
2. Maven Integration Test
Since we are getting close to real deployments, and we have hit unit integration tests pretty hard, I wanted to demonstrate a true integration test with the Maven integration test capabilities. Although this type of test takes more time to set up and execute, it places your application in a deployed simulation that can be tested locally before actual deployment. In this and follow-on chapters, we will use various realizations of the application and will start here with a simple Spring Boot Java application — absent of any backend resources.
Figure 1. Maven Integration Test
|
A Maven integration test is very similar to the other Web API unit integration tests you are used to seeing in this course. The primary difference is that there are no server-side components in the JUnit Spring context. All the server-side components are in a separate executable (coming from the same or dependency module). The following diagram shows the participants that directly help to implement the integration test. This will be accomplished with the aid of the Maven Failsafe, Spring Boot, and Build Maven Helper plugins. |
With that said, we will still want to be able to execute integration tests like this within the IDE (i.e., start long-running server(s) and execute JUnit tests within the IDE during active test development). Therefore, expect some setup aspects to support both IDE-based and Maven-based integration testing setup in the paragraphs that follow.
2.1. Maven Integration Test Phases
Maven executes integration tests using four (4) phases
-
pre-integration-test
- start resources -
integration-test
- execute tests -
post-integration-test
- stop resources -
verify
- evaluate/assert test results
These four (4) phases execute after the test
phase (immediately after the package
phase) and before the install
phase. We will make use of three (3) plugins to perform that work within Maven:
-
spring-boot-maven-plugin - used to start and stop the server-side Spring Boot process
-
build-maven-helper-plugin - used to allocate a random network port for server
-
maven-failsafe-plugin - used to run the JUnit JVM with the tests — passing in the port# — and verifying/asserting the results.
3. Maven Integration Test Plugins
3.1. Spring Boot Maven Plugin
The spring-boot-maven-plugin
will be configured with at least
two (2) additional executions to support Maven integration testing.
-
package - (existing) builds the Spring Boot Executable JAR (bootexec)
-
pre-integration-test - (new) start our Spring Boot application under test
-
post-integration-test - (new) stop our Spring Boot application under test
The following snippet just shows the outer shell of the plugin declaration.
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
... (1)
</executions>
</plugin>
1 | define goals, configuration, and phases for the plugin to perform |
3.1.1. spring-boot:repackage Goal
We have already been using the spring-boot-maven-plugin
to build an executable JAR from a Java JAR.
The build-app
execution was defined to run the repackage
goal during the Maven package
phase in ejava-build-parent/pom.xml to produce our Spring Boot Executable.
The global configuration element is used to define properties that are applied to all executions.
Each execution has a configuration element to define execution-specific configuration.
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration> (1)
<attach>${spring-boot.attach}</attach> (3)
<classifier>${spring-boot.classifier}</classifier> (2)
</configuration>
<executions>
<execution>
<id>build-app</id>
<phase>package</phase> (4)
<goals>
<goal>repackage</goal> (4)
</goals>
</execution>
</executions>
</plugin>
1 | global configuration applies to all executions |
2 | Executable JAR will get a unique suffix identified by classifier |
3 | Executable JAR will not be installed or deployed in any Maven local or remote repository when false |
4 | spring-boot-maven-plugin:repackage goal executed during the Maven package phase with global configuration |
3.1.2. spring-boot:start Goal
The next step is to start the built Spring Boot Executable JAR during the pre-integration
phase — to be ready for integration tests.
The following snippet shows an example of how to configure a follow-on execution of the Spring Boot Maven Plugin to start
the server in the background (versus a blocking run
).
The execution is configured to supply a Spring Boot server.port
property with the HTTP port to use.
We will define the port separately using the Maven server.http.port
property at build time.
The client-side @SpringBootTest
will also need this port value for the client(s) in the integration tests.
<execution>
<id>pre-integration-test</id> (1)
<phase>pre-integration-test</phase> (2)
<goals>
<goal>start</goal> (3)
</goals>
<configuration>
<skip>${skipITs}</skip> (4)
<arguments> (5)
<argument>--server.port=${server.http.port}</argument>
</arguments>
</configuration>
</execution>
1 | each execution must have a unique ID when there is more than one |
2 | this execution will be tied to the pre-integration-test phase |
3 | this execution will start the server in the background |
4 | -DskipITs=true will deactivate this execution |
5 | --server.port is being assigned at runtime and used by server for HTTP listen port |
skipITs support
Most plugins offer a skip option to bypass a configured execution and sometimes map that to a Maven property that can be expressed on the command line.
Failsafe maps their property to skipITs .
By mapping the Maven skipITs property to the plugin’s skip configuration element, we can inform related plugins to do nothing.
This allows one to run the Maven install phase without requiring integration tests to run and pass.
|
The above execution phase has the same impact as if we launched the JAR manually with --server.port
.
This allows multiple IT tests to run concurrently without colliding on network port number.
It also permits the use of a well-known/fixed value for use with IDE-based testing.
$ java -jar target/failsafe-it-example-*-SNAPSHOT-bootexec.jar
Tomcat started on port(s): 8080 (https) with context path ''(1)
$ java -jar target/failsafe-it-example-*-SNAPSHOT-bootexec.jar --server.port=7712 (2)
Tomcat started on port(s): 7712 (http) with context path '' (2)
1 | Spring Boot using well-known default port# |
2 | Spring Boot using runtime server.port property to override port to use |
As expected, the Spring Boot Maven Plugin has first-class support for many common Spring Boot settings. For example:
-
arguments - as just demonstrated
-
environment variables
-
JVM Arguments
-
profiles
-
etc.
You are not restricted to just command line arguments.
3.1.3. spring-boot:stop Goal
Running the tests is outside the scope of the Spring Boot Maven Plugin.
The next time we will need this plugin is to shut down the server during the post-integration-phase
— after the integration tests have completed.
The following snippet shows the Spring Boot Maven Plugin being used to stop
a running server.
<execution>
<id>post-integration-test</id> (1)
<phase>post-integration-test</phase> (2)
<goals>
<goal>stop</goal> (3)
</goals>
<configuration>
<skip>${skipITs}</skip> (4)
</configuration>
</execution>
1 | each execution must have a unique ID |
2 | this execution will be tied to the post-integration-test phase |
3 | this execution will stop the running server |
4 | -DskipITs=true will deactivate this execution |
At this point the Spring Boot Maven Plugin is ready to do what we need as long as we define the Maven server.http.port
property value to use.
We can define that statically with a Maven pom.xml property, a -Dserver.http.port= ####
on the Maven command line, or dynamically with the Maven Build Helper Plugin.
3.2. Build Helper Maven Plugin
The build-helper-maven-plugin
contains various utilities that are helpful to create a repeatable, portable build.
The snippet below shows the outer shell of the plugin declaration.
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
...
</executions>
</plugin>
We will populate the declaration with one execution to identify the available network port.
3.2.1. build-helper:reserve-network-port Goal
We will run the Build Helper Maven Plugin early in the lifecycle — during the process-resources
phase so that we have it early enough to use for unit tests (test
phase) as well as integration tests (pre-integration-test
phase when resources are started).
We are using the reserve-network-port
goal to select one or more random and available HTTP port numbers at build-time and assign each to a provided Maven property (using the portName
property).
The port number, in this case, is assigned to the server.http.port
Maven property.
The server.http.port
property was shown being used as input to the Spring Boot Maven Plugin earlier.
<execution>
<id>reserve-network-port</id>
<phase>process-resources</phase> (1)
<goals>
<goal>reserve-network-port</goal> (2)
</goals>
<configuration>
<portNames> (3)
<portName>server.http.port</portName>
</portNames>
</configuration>
</execution>
1 | execute during the process-resources Maven phase — which is well before pre-integration-test |
2 | execute the reserve-network-port goal of the plugin |
3 | assigned the identified port to the Maven server.http.port property |
The snippet below shows an example of the reserve-network-port
identifying an available network port.
[INFO] --- build-helper-maven-plugin:3.4.0:reserve-network-port (reserve-network-port) @ failsafe-it-example ---
[INFO] Reserved port 60066 for server.http.port
3.3. Failsafe Plugin
The Failsafe plugin is used to execute the JUnit tests.
It is a near duplicate of its sibling Surefire plugin, but targeted to operate in the Maven integration-test
phase.
The following snippet shows the outer shell of the Failsafe declaration.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
...
</executions>
</plugin>
3.3.1. failsafe:integration-test Goal
The failsafe:integration-test
goal executes the JUnit tests and automatically binds to the Maven integration-test
phase.
This execution is separate from the server started by the Spring Boot Maven Plugin but must have some common configuration in order to communicate.
In the snippet below, we are adding the integration-test
goal to Failsafe and configuring the plugin to launch the JUnit tests with an it.server.port
property.
<execution>
<id>integration-test</id>
<goals> (1)
<goal>integration-test</goal>
</goals>
<configuration>
<systemPropertyVariables> (2)
<it.server.port>${server.http.port}</it.server.port>
</systemPropertyVariables>
</configuration>
</execution>
1 | activate integration-test goal — automatically binds to integration-test phase |
2 | add a -Dit.server.port=${server.http.port} system property to the execution |
it.server.port used in ServerConfig used by Client
|
3.3.2. failsafe:verify Goal
The snippet below shows the final phase for Failsafe. After the integration resources have been taken down, the only thing left is to assert the results.
This is performed by the verify
Failsafe goal, which automatically binds to the Maven verify
phase.
This pass/fail assertion is delayed by a few Maven phases so that the build does not fail while integration resources are still running.
<execution>
<id>verify</id>
<goals> (1)
<goal>verify</goal>
</goals>
</execution>
1 | activate verify goal — automatically binds to verify phase |
verify Goal Must Be Declared to Report Pass/Fail to Maven Build
If the verify goal is not wired into the declaration — the setup, tests, and tear down will occur, but the build will not evaluate and report the results to Maven.
The integration tests will appear to always pass even if you see test errors reported in the integration-test output.
|
4. Integration Test Client
With the Maven aspects addressed, let’s take a look at any subtle changes we need to make to JUnit tests running within Failsafe.
4.1. JUnit @SpringBootTest
We start with a familiar looking JUnit test and @SpringBootTest
.
We can still leverage a Spring Context, however, there is no application or server-side resources in the Spring context.
The application has its Spring Context started by the Spring Boot Maven Plugin.
This Spring Context is for the tests running within the Failsafe Plugin.
@SpringBootTest(classes=ClientTestConfiguration.class, (1)
webEnvironment = SpringBootTest.WebEnvironment.NONE) (2)
public class FailsafeRestTemplateIT {
@Autowired
private RestTemplate authnUser;
@Autowired
private URI authnUrl;
1 | no application class in this integration test. Everything is server-side. |
2 | have only a client-side web environment. No listen port necessary |
4.2. ClientTestConfiguration
This trimmed down @Configuration
class is all that is needed for JUnit test to be a client of a remote process.
The @SpringBootTest
will demand to have a @SpringBootConfiguration
(@SpringBootApplication
is a @SpringBootConfiguration
) to initialize the application hosting the test.
So, we will assign that annotation to our test’s configuration.
We also need to add @EnableAutoConfiguration
(normally supplied by @SpringBootApplication
) to enable injection of external resources like RestTemplateBuilder
.
@SpringBootConfiguration(proxyBeanMethods = false) (1)
@EnableAutoConfiguration (2)
@Slf4j
public class ClientTestConfiguration {
...
@Bean
@ConfigurationProperties("it.server") (3)
public ServerConfig itServerConfig() { ...
@Bean
public URI authnUrl(ServerConfig serverConfig) { ...(4)
@Bean
public RestTemplate authnUser(RestTemplateBuilder builder,...(5)
...
1 | there must be 1 @SpringBootConfiguration and must be supplied when running without a @SpringBootApplication |
2 | must enable AutoConfiguration to trigger RestTemplateBuilder and other automatic resources |
3 | inject properties for test (e.g., it.server.port ) from Failsafe Maven Plugin |
4 | injectable baseUrl of remote server |
5 | injectable RestTemplate with authentication and HTTP filters applied |
Since Maven integration tests have no RANDOM_PORT and no late @LocalServerPort injection, bean factories for components that depend on the server port do not require @Lazy instantiation.
|
4.3. username/password Credentials
The following shows more of the @SpringBootConfiguration
with the username and password credentials being injected using values from the properties provided a test properties file.
In this test’s case — they should always be provided.
Therefore, no default String is defined.
public class ClientTestConfiguration {
@Value("${spring.security.user.name}")
private String username;
@Value("${spring.security.user.password}")
private String password;
4.4. ServerConfig
The following shows the primary purpose for ServerConfig
as a @ConfigurationProperties
class with flexible prefix.
In this particular case, it is being instructed to read in all properties with prefix "it.server" and instantiate a ServerConfig.
The Failsafe Maven Plugin is configured to supply the random port number using the it.server.port
property.
@Bean
@ConfigurationProperties("it.server")
public ServerConfig itServerConfig() {
return new ServerConfig();
}
The URL scheme will default to "http" and the port will default to "8080" unless a property override (it.server.scheme
, it.server.host
, and it.server.port
) is supplied.
The resulting value will be injected into the @SpringBootConfiguration
class.
4.5. authnUrl URI
Since we don’t have the late-injected @LocalServerPort
for the web-server and our ServerConfig
values are all known before starting the Spring Context, we no longer need @Lazy
instantiation.
The following shows the baseUrl
from ServerConfig
being used to construct a URL for "/api/authn/hello".
import org.springframework.web.util.UriComponentsBuilder;
...
@Bean
public URI authnUrl(ServerConfig serverConfig) {
URI baseUrl = serverConfig.getBaseUrl();
return UriComponentsBuilder.fromUri(baseUrl).path("/api/authn/hello").build().toUri();
}
4.6. authUser RestTemplate
We are going to create separate RestTemplate
components, fully configured to communicate and authenticate a particular way.
This one and only RestTemplate
for our example will be constructed by the authnUser
@Bean
factory.
By no surprise, authnUser()
@Bean
factory is adding a BasicAuthenticationInterceptor
containing the injected username and password to a new RestTemplate
for use in the test.
The injected ClientHttpRequestFactory
will take care of the HTTP/HTTPS details.
import org.springframework.http.client.BufferingClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.support.BasicAuthenticationInterceptor;
import org.springframework.web.client.RestTemplate;
...
@Bean
public RestTemplate authnUser(RestTemplateBuilder builder,
ClientHttpRequestFactory requestFactory) {
RestTemplate restTemplate = builder.requestFactory(
//used to read the streams twice -- so we can use the logging filter below
()->new BufferingClientHttpRequestFactory(requestFactory))
.interceptors(new BasicAuthenticationInterceptor(username, password), (1)
new RestTemplateLoggingFilter())
.build();
return restTemplate;
}
1 | adding a ClientHttpRequestInterceptor filter, supplied by Spring to supply an HTTP BASIC Authentication header with username and password |
4.7. HTTP ClientHttpRequestFactory
The ClientHttpRequestFactory
is supplied as a SimpleClientHttpRequestFactory
using injection to separate the underlying transport (TLS vs. non-TLS) from the HTTP configuration.
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
...
@Bean
public ClientHttpRequestFactory httpsRequestFactory() {
return new SimpleClientHttpRequestFactory(); (1)
}
1 | an HTTP/(non-TLS)-based ClientHttpRequestFactory |
4.8. JUnit @Test
The core parts of the JUnit test are pretty basic once we have the HTTP/Authn-enabled RestTemplate
and baseUrl injected.
From here it is just a normal test, except that the activity under test is remote on the server side.
public class FailsafeRestTemplateIT {
@Autowired (1)
private RestTemplate authnUser;
@Autowired (2)
private URI authnUrl;
@Test
public void user_can_call_authenticated() {
//given a URL to an endpoint that accepts only authenticated calls
URI url = UriComponentsBuilder.fromUri(authnUrl)
.queryParam("name", "jim").build().toUri();
//when called with an authenticated identity
ResponseEntity<String> response = authnUser.getForEntity(url, String.class);
//then expected results returned
then(response.getStatusCode()).isEqualTo(HttpStatus.OK);
then(response.getBody()).isEqualTo("hello, jim");
}
}
1 | RestTemplate with authentication and HTTP aspects addressed using filters |
2 | authnUrl built from ServerConfig and injected into test |
5. IDE Execution
JUnit tests, like all software, take a while to write and the development is more efficiently tested within the IDE.
The IDE can run the JUnit test shown here just like any other test.
The IDE does not care about class naming conventions.
The IDE will run any class method annotated with @Test
as a test.
However, the application must be manually started (and restarted after changes to src/main
).
There are at least two (2) ways to start the server when working with the JUnit test in the IDE.
In all cases, the port should default to something well-known like 8080
.
-
use the IDE to run the application class
-
use the Spring Boot Maven Plugin to run the application class. The
server.port
will default to8080
.Using Spring Boot Maven Plugin to Run Application$ mvn spring-boot:run ... [INFO] --- spring-boot-maven-plugin:3.3.2:run (default-cli) @ failsafe-it-example --- ... Tomcat started on port 8080 (http) with context path '/' Started FailsafeExampleApp in 1.63 seconds (process running for 1.862)
Supply an extra
spring-boot.run.arguments
system property to pass in any custom arguments, like port#.Using Spring Boot Maven Plugin to Run Application with Custom Port#$ mvn spring-boot:run -Dspring-boot.run.arguments='--server.port=7777' (1) ... [INFO] --- spring-boot-maven-plugin:3.3.2:run (default-cli) @ failsafe-it-example --- ... Tomcat started on port(s): 7777 (http) with context path '/' Started FailsafeExampleApp in 2.845 seconds (process running for 3.16)
1 use override to identify desired server.port
; otherwise use 8080
Once the application under test by the JUnit test is running, you can then execute individual integration tests annotated with @Test
, entire test cases, or entire packages of tests using the IDE.
Once the integration tests are working as planned, we can automate our work using Maven and the plugins we configured earlier.
6. Maven Execution
Maven can be used to automatically run integration tests by automating all external resource needs.
6.1. Maven Verify
When we execute mvn verify
(with an option to add clean
), we see the port being determined and assigned to the server.http.port
Maven property.
$ mvn verify
...(1)
- build-helper-maven-plugin:3.4.0:reserve-network-port (reserve-network-port) @ failsafe-it-example ---
[INFO] Reserved port 52024 for server.http.port
...(2)
- maven-surefire-plugin:3.3.2:test (default-test) @ failsafe-it-example ---
...
- spring-boot-maven-plugin:3.3.2:repackage (build-app) @ failsafe-it-example ---
...(3)
- spring-boot-maven-plugin:3.3.2:start (pre-integration-test) @ failsafe-it-example ---
...
1 | the port identified by build-helper-maven-plugin as 52024 |
2 | Surefire tests firing at an earlier test phase |
3 | server starting in the pre-integration-test phase |
mvn install also runs verify
The Maven verify goal is right before install .
Calling mvn clean install is common and runs all Surefire tests and Failsafe integration tests.
|
6.1.1. Server Output
When the server starts, we can see that the default profile is active and Tomcat was assigned the 52024
port value from the build.
FailsafeExampleApp#logStartupProfileInfo:634 No active profile set, falling back to 1 default profile: "default" (1) TomcatWebServer#initialize:108 Tomcat initialized with port(s): 52024 (http)(2)
1 | no profile has been activated on the server |
2 | server HTTP port assigned to 52024 |
6.1.2. JUnit Client Output
When the JUnit client starts, we can see that the baseURL contains http
and the dynamically assigned port 52024
.
FailsafeRestTemplateIT#logStartupProfileInfo:640 No active profile set, falling back to 1 default profile: "default" (1) ClientTestConfiguration#authnUrl:42 baseUrl=http://localhost:52024 (2) FailsafeRestTemplateIT#setUp:30 baseUrl=http://localhost:52024/api/authn/hello
1 | no profile is active in JUnit client |
2 | baseUrl is assigned http and port 52024 , with the latter dynamically assigned at build-time |
6.1.3. JUnit Test DEBUG
There is some DEBUG logged during the activity of the test(s).
GET /api/authn/hello?name=jim, headers=[accept:"text/plain, application/json, application/xml, text/xml, application/*+json, application/*+xml, */*", authorization:"Basic dXNlcjpwYXNzd29yZA==", user-agent:"masked", host:"localhost:52024", connection:"keep-alive"]]
6.1.4. Failsafe Test Results
Test results are reported.
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.168 s -- in info.ejava.examples.svc.https.FailsafeRestTemplateIT Results: Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
6.1.5. Server is Stopped
Server is stopped.
- spring-boot-maven-plugin:3.3.2:stop (post-integration-test) @ failsafe-it-example ---
Stopping application...
15:44:26.510 RMI TCP Connection(4)-127.0.0.1 INFO XBeanRegistrar$SpringApplicationAdmin#shutdown:159 Application shutdown requested.
6.1.6. Test Results Asserted
Test results are asserted.
[INFO] --- maven-failsafe-plugin:3.3.2:verify (verify) @ failsafe-it-example ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
7. Maven Configuration Reuse
The plugins can often become a mouthful to completely define, and we would like to conditionally reuse that definition and activate them in sibling modules using Maven integration tests. We can define them in the parent pom and make them optionally activated within Maven profiles and activated using various activation conditions.
-
JDK version
-
OS
-
Property presence and/or value
-
Packaging
-
File presence
-
manual activation (
-P profile_name
) or deactivation (-P !profile_name
)
One or more conditions can be defined. Since Maven 3.2.2, all conditions have been required to be satisfied to activate the profile.
Maven Profile Activation Criteria is ANDed
Maven profile activation lacks expressive predicate logic.
There is no option for OR .
|
Maven Profile is Separate from Spring Profile
Both Spring and Maven have the concept of a "profile".
The two are similar in concept but totally independent of one another.
Activating a Maven profile does not inherently activate a Spring profile and vice versa.
|
Bash Requires bang("!") to be Escaped
Linux/bash shells require a bang("!") character to be escaped. $ mvn clean install -P \!it |
7.1. it Maven profile
The following snippet shows the Shell of an it
Maven profile being defined to activate when the file application-it.properties
appears in the src/test/resources
directory.
We can place the plugin definitions from above into the profile.
We can also include other properties specific to the profile.
<profiles>
<profile>
<id>it</id> (1)
<activation>
<file> (2)
<exists>${basedir}/src/test/resources/application-it.properties</exists>
</file>
</activation>
<properties>
...
</properties>
<build>
<pluginManagement>
<plugins>
...
</plugins>
</pluginManagement>
<plugins> <!-- activate the configuration -->
...
</plugins>
</build>
</profile>
</profiles>
1 | defining profile named it |
2 | profile automatically activates when the specific file exists |
7.2. Failsafe HTTPS/IT Profile Example
I have added a second variant in a separate Maven Failsafe example module (failsafe-https-example
) that leverages the Maven it
profile in ejava-build-parent
.
The Maven profile activation only cares that the src/test/resources/application-it.properties
file exists.
The contents of the file is of no concern to the Maven it
profile itself.
The contents of application-it.properties
is only of concern to the Spring it
profile (when activated with @ActiveProfiles
).
7.2.1. it.properties Maven Trigger File
<profile>
<id>it</id>
<activation>
<file>
<exists>${basedir}/src/test/resources/application-it.properties</exists>
</file>
</activation>
|-- pom.xml
`-- src
...
`-- test
...
`-- resources
`-- application-it.properties (1)
1 | file presence (empty or not) triggers Maven profile |
7.2.2. Augmenting Parent Definition
This failsafe-https-example
example uses a Spring https
profile for the application and requires the Spring Boot Maven Plugin to supply that Spring profile when the application is launched.
We can augment the parent Maven plugin definition by specifying the plugin and execution ID with augmentations.
Specific options for configuration
can be found in the Spring Boot Maven Plugin run goal documentation
<build>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>pre-integration-test</id>
<configuration> (1)
<profiles>https</profiles>
</configuration>
</execution>
</executions>
</plugin>
1 | child pom augmenting parent configuration definition |
Additional Parent Plugin Override/Customization
We have some control over override versus append using combine.children and combine.self attributes of the configuration.
|
7.2.3. Integration Test Properties
The later failsafe-https-example
will require a few test properties to be defined.
We can use the application-it.properties
Maven profile activation file trigger to define them.
You will recognize them from the HTTPS Security lecture.
it.server.scheme=https
#must match self-signed values in application-https.properties
it.server.trust-store=${server.ssl.key-store}
it.server.trust-store-password=${server.ssl.key-store-password}
#used in IDE, overridden from command line during failsafe tests
it.server.port=8443
Same Basic HTTPS Setup Demonstrated with Surefire in the HTTPS Security Chapter
The example also requires the client Spring Context to be capable of establishing a TLS connection.
This is exactly as shown in the HTTPS Security chapter and will not be shown here.
See the failsafe-https-example project for the complete example these excerpts were taken from.
|
7.2.4. Activating Test Profiles
The JUnit test now activates two profiles: https
and it
.
-
the
https
profile is used to define the server communication properties thatapplication-it.properties
uses for templated property expansion. The client does not use these directly. -
the
it
profile is used to define the client communication properties.
Since the client property values are defined in terms of server properties, both profiles must be active to bring in both sources.
@SpringBootTest(classes=ClientTestConfiguration.class,
webEnvironment = SpringBootTest.WebEnvironment.NONE)
//https profile defines server properties used here by client
//it profile defines client-specific properties
@ActiveProfiles({"https","it"}) (1) (2)
public class HttpsRestTemplateIT {
1 | https profile activates server properties for client IT test context to copy |
2 | it profile defines client IT test properties templated using https defined properties |
7.2.5. Example HTTPS Failsafe Test Execution
The following mvn verify
output shows the complete build for the failsafe-https-example
project which leverages the reusable it
profile form the build parent.
$ mvn verify
- maven-resources-plugin:3.3.1:resources (default-resources)
- build-helper-maven-plugin:3.4.0:reserve-network-port (reserve-network-port)
Reserved port 59871 for server.http.port
- maven-compiler-plugin:3.11.0:compile (default-compile) @ https-hello-example ---
- maven-compiler-plugin:3.11.0:testCompile (default-testCompile) @ https-hello-example ---
- maven-surefire-plugin:3.1.2:test (default-test) @ https-hello-example ---
- maven-jar-plugin:3.3.0:jar (default-jar) @ https-hello-example ---
- maven-source-plugin:3.3.0:jar (attach-sources) @ https-hello-example ---
- spring-boot-maven-plugin:3.3.2:start (pre-integration-test) @ https-hello-example ---
...
HttpsExampleApp#logStartupProfileInfo:640 The following 1 profile is active: "https"
TomcatWebServer#initialize:108 Tomcat initialized with port(s): 59871 (https)
StandardService#log:173 Starting service [Tomcat]
...
- maven-failsafe-plugin:3.1.2:integration-test (integration-test)
...
Running info.ejava.examples.svc.https.HttpsRestTemplateIT
...
HttpsRestTemplateIT#logStartupProfileInfo:640 The following 2 profiles are active: "https", "it"
ClientTestConfiguration#authnUrl:55 baseUrl=https://localhost:59871
...
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
- spring-boot-maven-plugin:3.3.2:stop (post-integration-test)
[INFO] Stopping application...
- maven-failsafe-plugin:3.1.2:verify (verify)
----------------------------------------------------------------------
[INFO] BUILD SUCCESS
8. Summary
In this module, we learned:
-
the need for Maven integration tests
-
implement a Maven Failsafe integration test in order to test a Spring Boot executable JAR running as a stand-alone process
-
dynamically generate test instance-specific property values in order to be able to run in a shared CI/CD environment
-
start server process(es) configured with test instance-specific property values
-
execute tests against server process(es)
-
stop server process(es) at the conclusion of a test
-
evaluate test results once server process(es) have been stopped
-
-
how to define reusable Maven integration test setup and execution
-
generalize Maven constructs into a conditional Maven parent profile
-
implement and trigger a Maven integration test using a Maven parent profile
-