Mastodon

Testing Java HTTP Clients with MockWebServer

This article briefly introduces the MockWebServer, a nice way to test HTTP calls from a Java application.

While reviewing code in one of my major projects, I saw a developer using the org.springframework.web.reactive.function.client.WebClient. I never worked with this web client and thought that by accident, some reactive functionality was introduced. Turns out that the Spring WebClient is perfectly suited for traditional calls, as Baeldung states:

It’s important to note that even though it is, in fact, a non-blocking client and it belongs to the spring-webflux library, the solution offers support for both synchronous and asynchronous operations, making it suitable also for applications running on a Servlet Stack.

After deciding to keep the WebClient, I wanted to add tests for it and found MockWebServer, of course, also at Baeldung. ;) The Spring team themselves recommend using this library.

Here are some of the situations where MockWebServer can be a great help to write tests.

Setup

As always, add the dependencies:

<!-- Mocking Web Servers for Testing -->
<dependency>
	<groupId>com.squareup.okhttp3</groupId>
	<artifactId>okhttp</artifactId>
	<version>4.0.1</version>
	<scope>test</scope>
</dependency>
<dependency>
	<groupId>com.squareup.okhttp3</groupId>
	<artifactId>mockwebserver</artifactId>
	<version>4.0.1</version>
	<scope>test</scope>
</dependency>

This is a part of the service class making the HTTP calls via a org.springframework.web.reactive.function.client.WebClient, functionality omitted:

@Service
public class GVFService {

    private final WebClient myWebClient;

	public MyService(WebClient myWebClient) {
		this.myWebClient = myWebClient;
	}
...
}

In the test class, the backend has to be set up like this:

@BeforeAll
static void setUp() throws IOException {
	mockBackend = new MockWebServer();
	mockBackend.start();
}

@AfterAll
static void tearDown() throws IOException {
	mockBackend.shutdown();
}

@BeforeEach
void initialize() {

	String webClientBaseUrl = String.format("http://localhost:%s", mockBackend.getPort());

	myService = new MyService(WebClient.create(webClientBaseUrl));
}

Now, all calls from the service are executed against localhost on the port chosen by the testing framework.

Check HTTP Method

@Test
void retrievingTokenShouldSendPostRequest() throws InterruptedException {

	// Mock response
	mockBackend.enqueue(new MockResponse()
			.setBody("{\"value\":\"test\"}")
			.addHeader("Content-Type", "application/json"));

	// Execute request to locally running mock web server
	myService.getToken();

	// Validate request was send as expected
	RecordedRequest recordedRequest = mockBackend.takeRequest();
	assertEquals("POST", recordedRequest.getMethod());
}

Check If Bearer Token Is Sent

@Test
void retrievingTokenShouldSendClientCredentials() throws InterruptedException {

	// Mock response
	mockBackend.enqueue(new MockResponse()
			.setBody("{\"value\":\"test\"}")
			.addHeader("Content-Type", "application/json"));

	// Execute request to locally running mock web server
	myService.getToken();

	// Validate request was send as expected
	RecordedRequest recordedRequest = mockBackend.takeRequest();
	assertEquals("application/x-www-form-urlencoded;charset=UTF-8", recordedRequest.getHeader("Content-Type"));
	String bodyAsString = recordedRequest.getBody().readUtf8();
	assertEquals("client_id=42&client_secret=43", bodyAsString);
}

Check If URL Parameters Are Set

@Test
void requestingSomeValueSendsParametersAsURIPathVariables() throws InterruptedException, JsonProcessingException {

	// Mock response
	mockBackend.enqueue(new MockResponse()
			.setBody("{\"value\":\"test\"}")
			.addHeader("Content-Type", "application/json"));

	// Execute request to locally running mock web server
	JsonNode accessTokenAsJson = new ObjectMapper().readTree("{\"token\":\"my-bearer-token\"}");
	myService.executeRequest("my_value", accessTokenAsJson);

	// Validate request was send as expected
	RecordedRequest recordedRequest = mockBackend.takeRequest();

	assertNotNull(recordedRequest.getRequestUrl());
	assertEquals("value1", recordedRequest.getRequestUrl().pathSegments().get(0));
	assertEquals("value2", recordedRequest.getRequestUrl().pathSegments().get(1));
	assertEquals("value3", recordedRequest.getRequestUrl().pathSegments().get(2));
	assertEquals("my_value", recordedRequest.getRequestUrl().pathSegments().get(3));
}

Stubbing Response

Using the enqueue-method, the response to a request can even be mocked, see this example from Baeldung:

@Test
void getEmployeeById() throws Exception {
	Employee mockEmployee = new Employee(100, "Adam", "Sandler", 32, Role.LEAD_ENGINEER);
	mockBackEnd.enqueue(new MockResponse()
	.setBody(objectMapper.writeValueAsString(mockEmployee))
	.addHeader("Content-Type", "application/json"));
	
		Mono<Employee> employeeMono = employeeService.getEmployeeById(100);
	
		StepVerifier.create(employeeMono)
		  .expectNextMatches(employee -> employee.getRole()
			.equals(Role.LEAD_ENGINEER))
		  .verifyComplete();
}

(Image Public Domain, https://pxhere.com/de/photo/1380359)