Adding a MockWebServer JUnit Jupiter Extension

Posted on July 05, 2025 by Scott Leberknight

In the last post, I used several utilities in the kiwi-test library to clean up and remove boilerplate from tests using OkHttp's MockWebServer. But there's something else we can do to remove even more boilerplate from tests. The tests in the previous two blogs have the same code in the @BeforeEach and @AfterEach methods to:

  • Create a new MockWebServer instance and set an instance field
  • Get the base URI for the server where tests can send requests
  • Close the server after each test completes

This setup and teardown logic can be extracted into a JUnit Jupiter extension that will:

  • Before each test, create a new MockWebServer
  • Provide methods to get the server instance and the base URI of the server
  • After each test, close the server

Here is one implementation:

package org.kiwiproject.test.okhttp3.mockwebserver;

// imports...

public class MockWebServerExtension implements BeforeEachCallback, AfterEachCallback {

    @Getter
    @Accessors(fluent = true)
    private MockWebServer server;

    @Getter
    @Accessors(fluent = true)
    private URI uri;

    public MockWebServerExtension() {
        this(new MockWebServer());
    }

    public MockWebServerExtension(MockWebServer server) {
        this.server = KiwiPreconditions.requireNotNull(server, "server must not be nul");
    }

    @Override
    public void beforeEach(ExtensionContext context) throws IOException {
        server = new MockWebServer();
        server.start();
        uri = server.url("/").uri();
    }

    @Override
    public void afterEach(ExtensionContext context) {
        KiwiIO.closeQuietly(server);
    }
}

This implementation provides two constructors. The no-arg constructor creates a MockWebServer instance for you, while the one-arg constructor lets you create your own instance with any customization your tests need. For example, to support TLS.

It also provides the server() and uri() methods to easily get the MockWebServer instance and the base URI for use in your tests. Note these methods are generated usng Lombok, though they would be easy enough to create manually.

Using the extension in tests is straightforward. You add a MockWebServerExtension instance field and annotate it with @RegisterExtension:

@RegisterExtension
private final MockWebServerExtension serverExtension = new MockWebServerExtension();

For convenience, you can also declare a MockWebServer field:

private MockWebServer server;

Then in your test's @BeforeEach method, you initialize the server field, which can then be referenced in tests.

@BeforeEach
void setUp() {
    server = serverExtension.server();
    
    // additional initialization code...
}

Alternatively, you can get the server in each test using the extension's server() method:

@Test
void someTest() {
    var server = serverExtension.server();
    
    // test code...
}

Since the extension takes care of closing the server, you don't need to have a custom @AfterEach method to do that.

Now, you can write a complete test that uses the extension like the following:

class MathApiTest {

    @RegisterExtension
    private final MockWebServerExtension serverExtension = new MockWebServerExtension();
    
    private MathApiClient mathClient;
    private Client client;
    private MockWebServer server;
    
    @BeforeEach
    void setUp() {
        // Create the Jersey client
        client = ClientBuilder.newBuilder()
                .connectTimeout(500, TimeUnit.MILLISECONDS)
                .readTimeout(500, TimeUnit.MILLISECONDS)
                .build();
        
        server = serverExtension.server();
        var baseUri = serverExtension.uri();
        mathClient = new MathApiClient(client, baseUri);
    }

    @AfterEach
    void tearDown() {
        // Close the Jersey client
        client.close();
    }

    @Test
    void shouldAdd() {
        server.enqueue(new MockResponse()
                .setResponseCode(200)
                .setHeader(HttpHeaders.CONTENT_TYPE, "text/plain")
                .setBody("42"));

        assertThat(mathClient.add(40, 2)).isEqualTo(42);

        var recordedRequest = takeRequiredRequest(server);

        assertThatRecordedRequest(recordedRequest)
                .isGET()
                .hasPath("/math/add/40/2")
                .hasNoBody();
    }

    // ...more tests...
}

This test's @BeforeEach method gets the MockWebServer and the base URI directly from the MockWebServerExtension. So the only initialization logic it needs to do is to create a Jersey client and an instance of the class being tested, MathApiClient. As mentioned earlier, the test doesn't need to close the server in the @AfterEach method, so all it needs to do is close the Jersey client.

Each test then is the same as the previous post, where we used RecordedReqests and RecordedRequestAssertions from kiwi-test to keep the test code clean.

And that's all there is to it! The extension code shown above provides what you need in the majority of testing situations. But you don't need to create your own or copy this code if you don't want. kiwi-test version 3.9.0 adds its own MockWebServerExtension. It is very similar to the extension show here, but adds a few additional features such as the ability to specify a "server customizer", which is a Consumer<MockWebServer> that lets you customize a server, for example, to add TLS support and only support HTTP 1.1 and 2.0:

@RegisterExtension
private final MockWebServerExtension serverExtension = new MockWebServerExtension(svr -> {
    svr.setProtocols(List.of(Protocol.HTTP_2, Protocol.HTTP_1_1));
    svr.useHttps(getSocketFactory(), false);
});

It also provides a uri(path) method that lets you easily get a URI relative to the base URI of the server:

var statusURI = serverExtension.uri("/status");

Wrapping Up

Using a JUnit extension like the MockWebServerExtension shown here is one more thing you can do to eliminate boilerplate code in your tests. It can also provide the flexibility needed by different tests by allowing customization of the MockWebServer.



Post a Comment:
  • HTML Syntax: NOT allowed