Integration Testing in Spring Boot Application with Wiremock and Testcontainers
In this blog post we will see how to use Wiremock for Spring Boot integration testing using Testcontainers.
In one of my previous blog post, I have shown you how to mock external API using Wiremock library.In that blog post we have used annotation and programmatic approach to start the Wiremock server.
Wiremock server also can be started using Docker container and can be used for serve the mocking API.
Since Testcontainers library can be used to programmatically manage Docker containers, in this blog post I will show you how to start Wiremock server with Testcontainers and mock the external API.
Wiremock module is not officially supported by Testcontainers, so we will use library developed by community member thecheerfuldev.
Add the Wiremoc-Testcontainer module to project
<dependency>
<groupId>nl.thecheerfuldev</groupId>
<artifactId>testcontainers-wiremock</artifactId>
<version>1.17.0</version>
<scope>test</scope>
</dependency>
Code language: Java (java)
Add the Wiremock Dependency
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock</artifactId>
<version>3.0.0-beta-8</version>
<scope>test</scope>
</dependency>
Code language: Java (java)
Add the Maven Plugin
Add the maven failsafe plugin to run the integration test.
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.1.2</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
Code language: Java (java)
Creating Controller
Let’s develop a sample controller which calls some external API for our testing. I am using external API from https://jsonplaceholder.typicode.com
@RestController
@RequestMapping("/posts")
@Slf4j
public class PostController {
@Autowired
private RestOperations restTemplate;
@Value("${json.mock.api}")
private String jsonApi;
@PostMapping
public Post createPost(@RequestBody Post post) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Post> entity = new HttpEntity<Post>(post ,headers);
ResponseEntity<Post> response = restTemplate.exchange(jsonApi, HttpMethod.POST,entity,Post.class);
return response.getBody();
}
@GetMapping("/{id}")
public Post getPost(@PathVariable Integer id) {
try {
ResponseEntity<Post> response = restTemplate.getForEntity(jsonApi + "/" + id, Post.class);
if ( response.getBody() == null) {
throw new RestClientException("no information found");
}
return response.getBody();
} catch (HttpClientErrorException e) {
throw new ResponseStatusException(e.getStatusCode(),e.getMessage());
}
}
@PutMapping("/{id}")
public Post updatePost(@RequestBody Post post,@PathVariable Integer id) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Post> entity = new HttpEntity<Post>(post ,headers);
ResponseEntity<Post> response = null;
try {
response = restTemplate.exchange(jsonApi+"/"+id, HttpMethod.PUT,entity,Post.class);
} catch (HttpClientErrorException e) {
throw new ResponseStatusException(e.getStatusCode(),e.getMessage());
}
return response.getBody();
}
@DeleteMapping("/{id}")
public void deletePost(@PathVariable Integer id) {
try {
restTemplate.delete(jsonApi+"/"+id);
} catch (HttpClientErrorException e) {
throw new ResponseStatusException(e.getStatusCode(),e.getMessage());
}
}
@GetMapping
public List<Post> filterPostByUserId(@RequestParam Integer userId) {
ResponseEntity<Post[]> response;
try {
response = restTemplate.getForEntity(jsonApi+"?userId="+userId, Post[].class);
} catch (HttpClientErrorException e) {
throw new ResponseStatusException(e.getStatusCode(),e.getMessage());
}
if ( Objects.requireNonNull(response.getBody()).length == 0) {
throw new RestClientException("no information found");
}
return Arrays.stream(response.getBody()).toList();
}
}
Code language: Java (java)
Writing Integration Test
Let’s write a integration test for above controller.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@Slf4j
class WiremockExampleIT{
@Autowired
private TestRestTemplate testRestTemplate;
@LocalServerPort
private int serverPort;
@Container
private static final WireMockContainer WIRE_MOCK_CONTAINER = new
WireMockContainer("wiremock/wiremock:2.35.0-alpine") // - 1
.withStubMappingForClasspathResource("stubs") ; // - 2
......
@Test
public void testCreatePost() throws Exception {
Post p = new Post();
p.setBody("body");
p.setTitle("Title");
ResponseEntity<Post> response = testRestTemplate.postForEntity(ROOT_URL + serverPort + "/posts", p, Post.class);
assertEquals(200, response.getStatusCode().value());
assertNotNull( response.getBody().getId());
assertEquals(p.getBody(), response.getBody().getBody());
assertEquals(p.getTitle(), response.getBody().getTitle());
}
@DynamicPropertySource
public static void properties(DynamicPropertyRegistry registry) {
registry.add("json.mock.api", () -> WIRE_MOCK_CONTAINER.getHttpUrl() + "/posts");
}
}
Code language: Java (java)
1 – starts a Wiremock server based on the given docker image.
2 – stubs folder will contains all the request and response mapping for mocking API.
Next we have to create a “stubs” folder in src/test/resource path
stubs folder contains mappings for request matching and response in the form of templates. Templates are written in the json format.
Below is the template mapping for HTTP POST method.
{
"mappings": [
{
"request": {
"method": "POST",
"url": "/posts"
},
"response": {
"status": 201,
"headers": {
"content-type": "application/json"
},
"jsonBody": {
"body": "body response",
"title": "title response",
"userId": "{{randomValue length=5 type='NUMERIC'}}",
"id": 1
}
}
}
}
Code language: Java (java)
Enabling Wiremock Extensions
Wiremock can be extended to transform the response to send dynamic data. We can capture the data from the request and request body and can include it as part of the response to make the response dynamic.
Wiremock extensions are useful when we are expecting dynamic data from the response. For example when you create a entity using http post request, you want to verify that response contains same data as sent in request body.
JsonBodyTransformer – extension extends Wiremock to capture json data from request body and include it as part of response.
ResponseTemplateTransformer – extension extends Wiremock to capture data from request data like path variables, query params and headers.
WireMock extensions are packaged as JAR files. In order to use them they need to be made available at runtime and WireMock docker image must be configured to enable them by adding –extension <class-name> command line.
For example, to use the JsonBodyTransformer extension we would first download com.ninecookies.wiremock.extensions:wiremock-extensions.jar and copy the jar into “/var/wiremock/extensions” folder inside container
@Container
private static final WireMockContainer WIRE_MOCK_CONTAINER = new WireMockContainer("wiremock/wiremock:2.35.0-alpine")
.withCopyFileToContainer(MountableFile.forClasspathResource("extensions"), "/var/wiremock/extensions")
.withStubMappingForClasspathResource("stubs") //// loads all *.json files in resources/stubs/
.withCommand("-verbose")
.withCommand("--gloabl-response-templating")
.withCommand("--extensions dev.fullstackcode.wiremock.transformer.ResponseTemplateTransformerExtension,com.ninecookies.wiremock.extensions.JsonBodyTransformer");
Code language: Java (java)
Note
You can find more details about this issue in Troubleshoot section
Exception in thread “main” java.lang.NoSuchMethodException: com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer.<init>()
In above code snippet we are enabling 2 extensions, JsonBodyTransformer and ResponseTemplateTransformer .
To download JsonTranformer extension jar we use the maven-dependency-plugin.
As the “ResponseTemplateTransformer” is part of Wiremock it self I am also downloading wiremock jar.
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<id>copy</id>
<phase>package</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>com.ninecookies.wiremock.extensions</groupId>
<artifactId>wiremock-extensions</artifactId>
<version>0.4.1</version>
<classifier>jar-with-dependencies</classifier>
</artifactItem>
<artifactItem>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock</artifactId>
<version>3.0.0-beta-8</version>
</artifactItem>
</artifactItems>
<outputDirectory>${project.build.directory}/test-classes/extensions</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
Code language: Java (java)
Now you can change response template. we can use these transformers and make the response dynamic .
{
"mappings": [
{
"request": {
"method": "POST",
"url": "/posts"
},
"response": {
"status": 201,
"headers": {
"content-type": "application/json"
},
"jsonBody": {
"body": "$(body)",
"title": "$(title)",
"userId": "{{randomValue length=5 type='NUMERIC'}}",
"id": 1
},
"transformers" : ["json-body-transformer","response-template"]
}
},
{
"request": {
"method": "PATCH",
"url": "/posts/1"
},
"response": {
"status": 200,
"headers": {
"content-type": "application/json"
},
"jsonBody": {
"body": "$(body)",
"title": "$(title)",
"userId": "{{randomValue length=5 type='NUMERIC'}}",
"id": "${{request.path.[1]}}"
},
"transformers" : ["json-body-transformer","response-template"]
}
}
]
Code language: Java (java)
Troubleshooting
The actual class for enabling ResponseTemplateTransformer is
com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer
Code language: Java (java)
But when I am trying to activate the extension using the above class I was getting below error
as the class was missing default constructor.
Exception in thread "main" java.lang.NoSuchMethodException: com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer.<init>()
Code language: Java (java)
To resolve the issue, I have to create separate Java project with single class containing default constructor which extends the ResponseTemplateTransformer class.
package dev.fullstackcode.wiremock.transformer;
import com.github.tomakehurst.wiremock.extension.responsetemplating.ResponseTemplateTransformer;
public class ResponseTemplateTransformerExtension extends ResponseTemplateTransformer {
public ResponseTemplateTransformerExtension() {
super(false);
}
}
Code language: Java (java)
I could not place the above class in same project as Wiremock docker image was accepting jar built with Java 11 version only.
Built jar file is place in src/test/resource folder and copied to resource folder during build using maven resources plugins.
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>copy-resource-one</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/test-classes/extensions</outputDirectory>
<resources>
<resource>
<directory>${project.basedir}/src/test/resources</directory>
<includes>
<include>wiremock-responsetemplate-extension-1.0-SNAPSHOT.jar</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
Code language: Java (java)
Then jar files are copied inside container from resource folder.
You can download source code for the blog post from GitHub