Integration Tests are as important as unit tests. They validate a feature from a higher level, and involve more components or layers at the same time. Integration tests (IT tests) are given more importance when an environment needs to evolve fast. Design processes often require iterations, and unit tests sometimes seriously impact our ability to refactor, while higher level testing is less impacted comparatively.
This recipe shows how to develop automated IT tests that focus on Spring MVC web services. Such IT Tests are not behavioral tests as they don't assess the user interface at all. To test behaviors, an even higher testing level would be necessary, simulating the User journey through the application interface.
We will configure the Cargo Maven Plugin to stand up a test environment as part of the pre-integration-test Maven phase. On the integration-test phase, we will get the Maven failsafe plugin to execute our IT Tests. Those IT Tests will make use of the REST-assured library to run HTTP requests against the test environment and assert the HTTP responses.
cloudstreetmarket-api
module. These tests are intended to test the API controller methods.<dependency> <groupId>com.jayway.restassured</groupId> <artifactId>rest-assured</artifactId> <version>2.7.0</version> </dependency>
UserControllerIT.createUserBasicAuth()
:public class UserControllerIT extends AbstractCommonTestUser{ private static User userA; @Before public void before(){ userA = new User.Builder() .withId(generateUserName()) .withEmail(generateEmail()) .withCurrency(SupportedCurrency.USD) .withPassword(generatePassword()) .withLanguage(SupportedLanguage.EN) .withProfileImg(DEFAULT_IMG_PATH) .build(); } @Test public void createUserBasicAuth(){ Response responseCreateUser = given() .contentType("application/json;charset=UTF-8") .accept("application/json"") .body(userA) .expect .when() .post(getHost() + CONTEXT_PATH + "/users"); String location = responseCreateUser.getHeader("Location"); assertNotNull(location); Response responseGetUser = given() .expect().log().ifError() .statusCode(HttpStatus.SC_OK) .when() .get(getHost() + CONTEXT_PATH + location + JSON_SUFFIX); UserDTO userADTO = deserialize(responseGetUser.getBody().asString()); assertEquals(userA.getId(), userADTO.getId()); assertEquals(userA.getLanguage().name(), userADTO.getLanguage()); assertEquals(HIDDEN_FIELD, userADTO.getEmail()); assertEquals(HIDDEN_FIELD, userADTO.getPassword()); assertNull(userA.getBalance()); } }
integration
.cloudstreetmarket-api pom.xml
file:<profiles> <profile> <id>integration</id> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <version>2.12.4</version> <configuration> <includes> <include>**/*IT.java</include> </includes> <excludes> <exclude>**/*Test.java</exclude> </excludes> </configuration> <executions> <execution> <id>integration-test</id> <goals> <goal>integration-test</goal> </goals> </execution> <execution> <id>verify</id> <goals><goal>verify</goal></goals> </execution> </executions> </plugin> <plugin> <groupId>org.codehaus.cargo</groupId> <artifactId>cargo-maven2-plugin</artifactId> <version>1.4.16</version> <configuration> <wait>false</wait> <container> <containerId>tomcat8x</containerId> <home>${CATALINA_HOME}</home> <logLevel>warn</logLevel> </container> <deployer/> <type>existing</type> <deployables> <deployable> <groupId>edu.zc.csm</groupId> <artifactId>cloudstreetmarket-api</artifactId> <type>war</type> <properties> <context>api</context> </properties> </deployable> </deployables> </configuration> <executions> <execution> <id>start-container</id> <phase>pre-integration-test</phase> <goals> <goal>start</goal> <goal>deploy</goal> </goals> </execution> <execution> <id>stop-container</id> <phase>post-integration-test</phase> <goals> <goal>undeploy</goal> <goal>stop</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </profile> </profiles>
C: omcat8
: on MS Windows/home/usr/{system.username}/tomcat8
: on Linux/Users/{system.username}/tomcat8
: on Mac OS X mvn clean verify -P integration
If all the tests pass, you should see the [INFO] BUILD SUCCESS
message.
We will explain in this section why we have introduced the Maven failsafe plugin, how the Cargo Plugin configuration satisfies our needs, how we have used REST-assured, and how useful this REST-assured library is.
We are using Maven failsafe to run Integration tests and Maven Surefire for unit tests. This is a standard way of using these plugins. The following table reflects this point, with the Plugins' default naming patterns for test classes:
Maven Surefire |
Maven Failsafe | |
---|---|---|
Default tests inclusion patterns |
|
|
Default output directory |
|
|
Bound to build phase |
|
|
For Maven Failsafe, you can see that our overridden pattern inclusion/exclusion was optional. About the binding to Maven build phases, we have chosen to trigger the execution of our integration tests on the integration-test
and verify
phases.
Cargo is a lightweight library that offers standard API for operating several supported containers (Servlet and JEE containers). Examples of covered API operations are artifacts' deployments, remote deployments and container start/stop. When used through Maven, Ant, or Gradle, it is mostly used for its ability to provide support to Integration Tests but can also serve other scopes.
We have used Cargo through its Maven plugin org.codehaus.cargo:cargo-maven2-plugin
to automatically prepare an integration environment that we can run integration tests against. After the integration tests, we expect this environment to shut down.
The following executions have been declared as part of the cargo-maven2-plugin
configuration:
<executions> <execution> <id>start-container</id> <phase>pre-integration-test</phase> <goals> <goal>start</goal> <goal>deploy</goal> </goals> </execution> <execution> <id>stop-container</id> <phase>post-integration-test</phase> <goals> <goal>undeploy</goal> <goal>stop</goal> </goals> </execution> </executions>
Let's visit what happens when the mvn install
command is executed.
The install
is a phase of the default Maven life cycle. As explained in Chapter 1, Setup Routine for an Enterprise Spring Application
, the default life cycle has 23 build phases from validate
to deploy
. The install
phase is the 22nd, so 22 phases are checked to see whether there are plugin goals that could be attached to them.
Here, the pre-integration-test
phase (that appears in the default life cycle between validate
and install
) will trigger the processes that are located under the start
and deploy
goals of our maven Cargo plugin. It is the same logic with post-integration-test
triggers the undeploy
and stop
goals.
Before the IT tests execution, we start and deploy the Tomcat server. These IT tests are processed with Maven failsafe in the integration-test
phase. Finally, the Tomcat server is undeployed and stopped.
IT Tests can also be executed with the verify
phase (if the server is started out of the default Maven life cycle).
In the Cargo Maven plugin configuration, we are targeting an existing instance of Tomcat. Our application is currently depending upon MySQL, Redis, Apache HTTP, and a custom session management. We have decided that the IT Tests execution will be required to be run in a proper integration environment.
Without all these dependencies, we would have got Cargo to download a Tomcat 8 instance.
REST-assured is an open source library licensed Apache v2 and supported by the company Jayway. It is written with Groovy and allows making HTTP requests and validating JSON or XML responses through its unique functional DSL that drastically simplify the tests of REST services.
To effectively use REST-assured, the documentation recommends adding static imports of the following packages:
com.jayway.restassured.RestAssured.*
com.jayway.restassured.matcher.RestAssuredMatchers.*
org.hamcrest.Matchers.*
To understand the basics of the REST-assured DSL, let's consider one of our tests (in UserControllerIT
) that provides a short overview of REST-assured usage:
@Test public void createUserBasicAuthAjax(){ Response response = given() .header("X-Requested-With", "XMLHttpRequest") .contentType("application/json;charset=UTF-8") .accept("application/json") .body(userA) .when() .post(getHost() + CONTEXT_PATH + "/users"); assertNotNull(response.getHeader("Location")); }
The given
part of the statement is the HTTP Request specification. With REST-assured, some request headers like Content-Type
or Accept
can be defined in an intuitive way with contentType(…)
and accept(…)
. Other headers can be reached with the generic .header(…)
. Request parameters and authentication can also be defined in a same fashion.
For POST
and
PUT
requests, it is necessary to pass a body to the request. This body
can either be plain JSON or XML or directly the Java object (as we did here). This body
, as a Java object, will be converted by the library depending upon the content-type
defined in the specification (JSON or XML).
After the HTTP Request specification, the when()
statement provides information about the actual HTTP method and destination.
At this stage, the returned object allows us either to define expectations from a then()
block or, as we did here, to retrieve the Response
object from where constraints can be defined separately. In our test case, the Location
header of the Response
is expected to be filled.
More information can be found at the following Cargo and REST-assured respective documentations:
For more information about the product and its integration with third-party systems, refer to https://codehaus-cargo.github.io/cargo/Home.html.