Building a Stub Server using WireMock and Spring Boot
WireMock is a stub framework that helps you create stubs for outgoing HTTP traffic during your tests. Most people use WireMock in their test suite during build time of the application. Spin up the WireMock server, configure some stub rules, run the application tests, and tear everything down. This is a good way of testing your HTTP clients, using real traffic towards an external server.
What about testing the application when deployed on a cloud platform? That is even a more realistic context than runnings tests against an application deployed on the local machine of a developer laptop or a build agent. You probably want such a test in isolation, so not using a complete integrated system of different applications. To achieve this, one still needs those stubbed responses but now from an external server called a stub server.
Why use Spring Boot together with WireMock? Because you already work with them for your applications. You probably use health probes for liveness and readiness provided by Spring Boot Actuator, some authentication via Spring Security, and use the WireMock DSL in your tests. Our motivation: Use the tech stack a team is already familiar with.
Setting up the Stub Server
We will show a most basic example, that can be extended using all the cool features of Spring Boot and WireMock you already know.
The Maven configuration (located pom.xml
):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jdriven</groupId>
<artifactId>stub-server</artifactId>
<version>${revision}</version>
<properties>
<java.version>17</java.version>
<spring-boot.version>2.7.7</spring-boot.version>
<wiremock.version>2.35.0</wiremock.version>
</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-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>${wiremock.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
The Spring Boot Application (located src/main/java/com/jdriven/stubserver/StubApplication.java
):
package com.jdriven.stubserver;
import com.github.tomakehurst.wiremock.servlet.WireMockHandlerDispatchingServlet;
import com.github.tomakehurst.wiremock.servlet.WireMockWebContextListener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.EventListener;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.servlet.mvc.ServletWrappingController;
import java.util.Properties;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching;
@SpringBootApplication
public class StubApplication {
public static void main(String[] args) {
SpringApplication.run(StubApplication.class, args);
}
@Bean
WireMockWebContextListener WireMockWebContextListener() {
return new WireMockWebContextListener();
}
@Bean
ServletWrappingController wireMockController() {
ServletWrappingController controller = new ServletWrappingController();
controller.setServletClass(WireMockHandlerDispatchingServlet.class);
controller.setBeanName("wireMockController");
Properties properties = new Properties();
properties.setProperty("RequestHandlerClass", "com.github.tomakehurst.wiremock.http.StubRequestHandler");
controller.setInitParameters(properties);
return controller;
}
@Bean
SimpleUrlHandlerMapping wireMockControllerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
Properties urlProperties = new Properties();
urlProperties.put("/*", "wireMockController");
mapping.setMappings(urlProperties);
mapping.setOrder(Integer.MAX_VALUE - 1);
return mapping;
}
@Bean
ServletWrappingController wireMockAdminController() {
ServletWrappingController controller = new ServletWrappingController();
controller.setServletClass(WireMockHandlerDispatchingServlet.class);
controller.setBeanName("wireMockAdminController");
Properties properties = new Properties();
properties.setProperty("RequestHandlerClass", "com.github.tomakehurst.wiremock.http.AdminRequestHandler");
controller.setInitParameters(properties);
return controller;
}
@Bean
SimpleUrlHandlerMapping wireMockAdminControllerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
Properties urlProperties = new Properties();
urlProperties.put("/__admin/*", "wireMockAdminController");
mapping.setMappings(urlProperties);
mapping.setOrder(Integer.MAX_VALUE - 2);
return mapping;
}
@EventListener(ApplicationReadyEvent.class)
void configureStubs() {
stubFor(get(urlPathMatching("/hello-world")).willReturn(aResponse()
.withHeader("Content-Type", "text/plain")
.withBody("Hello World")));
}
}
The Spring Boot configuration (located src/main/resources/application.yml
):
server:
port: 8080
servlet:
context-parameters:
WireMockFileSourceRoot: '/WEB-INF/wiremock'
Basically we use the servlet support of WireMock together with the ability of Spring to wrap a servlet definition and act like the servlet is deployed to the application server.
The incoming requests are handled by Spring first, so for example Spring Security can do some magic, and then handed to the defined controllers.
Two servlet definitions are required: one for the regular incoming traffic that get matched against stub definitions, and the other for admin task related traffic.
It is important that the /*
mapping has the last in order of matching so the WireMock admin requests, but also for example Spring Actuator requests, match first to their corresponding controllers.
After the application becomes ready to receive traffic the stub configuration will run, which sends admin requests to the application server.
Running the Stub Server
Try it out:
# Start the application
$ mvn spring-boot:run
# Sending a request to an endpoint stubbed by WireMock
$ curl http://localhost:8080/hello-world
Hello world
# Sending a request to an endpoint used for a Spring Boot feature
$ curl http://localhost:8080/actuator/health
{"status":"UP"}
Alternatives
-
You could use the
new WireMockServer()
feature of WireMock. It will start an application server just like Spring Boot. However, it cannot use the application server of Spring Boot. It will result in two application servers running. The downside is that the traffic has to be routed to the port of the desired framework (either Spring or WireMock). Traffic forwarded to WireMock will not pass through Spring interceptors and therefore Spring Security does not work for that traffic. -
You could use the
DirectCallHttpServer
feature of WireMock. TheDirectCallHttpServer
is a way to start WireMock without an application server. Create a Spring Controller that maps all the endpoints and forward the request to theDirectCallHttpServer
. Unfortunately there is no existing mapping for request and response models of Spring and WireMock. You would have to implement one yourself for all the possible features in the HTTP specification.
Conclusions
The servlet feature of WireMock allows us to bind Spring Boot and WireMock, working together on one application server. This can be used for a deployable stub server with all the possible features of both Spring Boot and WireMock.