[Spring Boot] Using WireMock and MockWebServer for Spring WebFlux Integration Tests
This article covers how to configure WireMock
and MockWebServer
for a Spring WebFlux application to stub for any external APIs the application might need.
Implementation
Currently, WebClient
is used for calling the external APIs.
@Configuration
public class HttpProxyConfiguration {
@Value("${tracker.url}")
private String trackerUrl;
@Bean
TrackerClient trackerClient(WebClient.Builder builder) {
var wc = builder.baseUrl(trackerUrl).build();
var wca = WebClientAdapter.forClient(wc);
return HttpServiceProxyFactory.builder()
.clientAdapter(wca)
.build()
.createClient(TrackerClient.class);
}
}
WireMock
Dependency
For setting up WireMock
, spring-cloud-contract-wiremock
dependency will be used. Because this is a spring-cloud
dependency, the version and dependency management for spring-cloud
have to be configured as well.
// plugins, group, version, java, repositories
extra["springCloudVersion"] = "2022.0.4"
dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-webflux")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.projectreactor:reactor-test")
testImplementation("org.springframework.cloud:spring-cloud-contract-wiremock")
}
dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
}
}
TestCode
Here are the steps on how this works:
@AutoConfigureWireMock
is used to start a WireMock server as part of the Spring Application Context and WireMock server properties will be mapped towiremock.server
.- Then, the external API endpoint can be replaced to the started WireMock server by overriding the application properties to
http://localhost:${wiremock.server.port}
. - Start adding stubs for every API endpoint using
wireMockServer.stubFor()
. - Happy Testing!
import com.github.tomakehurst.wiremock.WireMockServer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.reactive.server.WebTestClient;
import java.io.IOException;
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.post;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.springframework.http.HttpStatus.OK;
@AutoConfigureWireMock(port = 0)
@TestPropertySource(properties = {
"tracker.url=http://localhost:${wiremock.server.port}"
})
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class JourneyTests {
@Autowired
WebTestClient wtc;
@Autowired
WireMockServer wireMockServer;
@Test
void test() throws IOException {
wireMockServer.stubFor(get(urlPathEqualTo("/first_api_path"))
.willReturn(aResponse()
.withStatus(OK.value())
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.withBody(new ClassPathResource("first_api_call.json").getContentAsString(UTF_8))));
wireMockServer.stubFor(post(urlPathEqualTo("/second_api_path"))
.willReturn(aResponse()
.withStatus(OK.value())
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.withBody(new ClassPathResource("second_api_call.json").getContentAsString(UTF_8))));
List<Project> response = wtc.get()
.uri("/api/projects")
.exchange()
.expectBodyList(Project.class)
.returnResult()
.getResponseBody();
assertThat(response.size()).isEqualTo(1);
// ... other assertions
}
}
MockWebServer
Dependency
For setting up MockWebServer
, a couple of dependencies need to be added.
// plugins, group, version, java, repositories
dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-webflux")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.projectreactor:reactor-test")
testImplementation("com.squareup.okhttp3:okhttp")
testImplementation("com.squareup.okhttp3:mockwebserver")
}
Test Code
Here are the steps on how this works:
- First, ask Java for an unused port using
new ServerSocket(0)
. We will use this port for spinning up a newMockWebServer
. - Then, change the external API url for this test using
@DynamicPropertySource
. - Start
MockWebServer
on that unused port which we got from the first step. - Start adding stubs for the external APIs using
mockWebServer.enqueue()
. - Happy testing!
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.reactive.server.WebTestClient;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.List;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.http.HttpStatus.OK;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class JourneyTests {
static int MOCK_SERVER_PORT;
static {
try (var serverSocket = new ServerSocket(0)) {
MOCK_SERVER_PORT = serverSocket.getLocalPort();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
MockWebServer mockWebServer;
@Autowired
WebTestClient wtc;
@DynamicPropertySource
static void trackerProperties(DynamicPropertyRegistry registry) {
registry.add("tracker.url", () -> "http://localhost:" + MOCK_SERVER_PORT);
}
@BeforeEach
void beforeEach() throws IOException {
mockWebServer = new MockWebServer();
mockWebServer.start(MOCK_SERVER_PORT);
}
@AfterEach
void afterEach() throws IOException {
mockWebServer.shutdown();
}
@Test
void test() throws IOException {
var firstJsonResponse = new MockResponse()
.setResponseCode(OK.value())
.setBody(new ClassPathResource("first_api_call.json").getContentAsString(UTF_8))
.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
mockWebServer.enqueue(firstJsonResponse);
var secondJsonResponse = new MockResponse()
.setResponseCode(OK.value())
.setBody(new ClassPathResource("second_api_call.json").getContentAsString(UTF_8))
.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
mockWebServer.enqueue(secondJsonResponse);
List<Project> response = wtc.get()
.uri("/api/projects")
.exchange()
.expectBodyList(Project.class)
.returnResult()
.getResponseBody();
assertEquals(1, response.size());
// ... other assertions
}
}