Testing HTTP Client Code with MockWebServer

Posted on January 15, 2025 by Scott Leberknight

When testing HTTP client code, it can be challenging to verify your application's behavior. For example, if you have an HTTP client that makes calls to some third-party API, or even to another service that you control, you want to make sure that you are sending the correct requests and handling the responses properly. There are various libraries available to help, and many times the library or framework you're using provides some kind of test support.

For example, I've used Dropwizard to create REST-based web services for a number of years. Dropwizard uses Jersey, which is the reference implementation of Jakarta RESTful Web Services (formerly known as JAX-RS). Dropwizard provides a way to test HTTP client implementations by creating a resource within your test that acts as a "test double" of the real server you are trying to simulate. When the test executes, a real HTTP server is started that can respond to real HTTP requests. No mocking, which is important since mocks can't easily simulate all the various things that can happen with HTTP requests.

Suppose you have an HTTP client that uses Jersey Client to call a "Math API". For now, you only care about adding two numbers, so your client looks like:

public class MathApiClient {

    private final Client client;
    private final URI baseUri;

    public MathApiClient(Client client, URI baseUri) {
        this.client = client;
        this.baseUri = baseUri;
    }

    public int add(int a, int b) {
        var response = client.target(baseUri)
                .path("/math/add/{a}/{b}")
                .resolveTemplate("a", a)
                .resolveTemplate("b", b)
                .request()
                .get();

        return response.readEntity(Integer.class);
    }
}

You want to design the client for easy testing, so the constructor accepts a Jersey Client and a URI, which lets you easily change the target server location. That's important, since you need to be able to provide the URI of the test server.

Here's an example of a Math API test class using Dropwizard's integration testing support:

@ExtendWith(DropwizardExtensionsSupport.class)
class DropwizardMathApiClientTest {

    @Path("/math")
    public static class MathStubResource {
        @GET
        @Path("/add/{a}/{b}")
        @Produces(MediaType.TEXT_PLAIN)
        public Response add(@PathParam("a") int a, @PathParam("b") int b) {
            var answer = a + b;
            return Response.ok(answer).build();
        }
    }

    private static final DropwizardClientExtension CLIENT_EXTENSION =
            new DropwizardClientExtension(new MathStubResource());

    private MathApiClient mathClient;
    private Client client;

    @BeforeEach
    void setUp() {
        client = ClientBuilder.newBuilder()
                .connectTimeout(500, TimeUnit.MILLISECONDS)
                .readTimeout(500, TimeUnit.MILLISECONDS)
                .build();
        var baseUri = CLIENT_EXTENSION.baseUri();
        mathClient = new MathApiClient(client, baseUri);
    }

    @AfterEach
    void tearDown() {
        client.close();
    }

    @Test
    void shouldAdd() {
        assertThat(mathClient.add(40, 2)).isEqualTo(42);
    }
}

In this code, it's the DropwizardClientExtension that provides all the real HTTP server functionality. You provide it the stub resource (a new MathStubResource instance) and it takes care of starting a real application that responds to HTTP requests and responds as you defined in the stub resource. Then you write tests that use the MathApiClient, make assertions as you normally would, and so on.

This works great, but there are some downsides. First, there is no way to (easily) verify the HTTP requests that the HTTP client made. The client makes the HTTP request and handles the response, but unless it provides some way to access the requests it has made, there's not really any way to verify this. You can add code into the stub resource to capture the requests, and provide a way for test code to access them, but that adds complexity to the stub resource.

Second, while testing the "happy path" is straightforward, things quickly become more difficult if you want to test errors, invalid input, and other "not happy path" scenarios. For example, let's say you want to test how your client responds when it receives an error response such as a 400 Bad Request or 500 Internal Server Error. How can you do this? One way is "magic input" where the server responds with a 400 when you provide one set of input (e.g., whenever a is 84) and a 500 when you provide a different input (e.g., whenever a is 142). Depending on the number of error cases you want to test, the stub resource code can quickly get complicated with conditionals. Another way is to use some kind of "flag" field inside the test stub resource class, where each test can "record" the response it wants. But this starts to become a "mini-framework" as you need more and more features.

Something else you can do is to create separate tests with different stub resources for different scenarios. But again, this can get out of control quickly if your HTTP client has a lot of methods and you want to test each one thoroughly.

Despite these shortcomings, you can still write good HTTP tests using what Dropwizard (and other similar libraries) provides. I've used the Dropwizard test support for the vast majority of HTTP client testing over the past few years. But I've recently come across the excellent MockWebServer from OkHttp. Basically, it is like a combination of a real HTTP server to test against and a mocking library such as Mockito.

To test HTTP clients using MockWebServer, you:

  1. Record the responses you want to receive
  2. Run your HTTP client code
  3. Make assertions about the result from the client (if any)
  4. Verify the client made the expected requests

This is very similar to using mocking like in Mockito, except that MockWebServer lets you test against the full HTTP/HTTPS request/response lifecycle in a realistic manner. So, rewriting the above test to use MockWebServer looks like:

class OkHttpMathApiClientTest {

    private MathApiClient mathClient;
    private Client client;
    private MockWebServer server;

    @BeforeEach
    void setUp() throws URISyntaxException {
        client = ClientBuilder.newBuilder()
                .connectTimeout(500, TimeUnit.MILLISECONDS)
                .readTimeout(500, TimeUnit.MILLISECONDS)
                .build();

        server = new MockWebServer();
        var baseUri = server.url("/").uri();

        mathClient = new MathApiClient(client, baseUri);
    }

    @AfterEach
    void tearDown() throws IOException {
        client.close();
        server.close();
    }

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

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

        var recordedRequest = server.takeRequest(1, TimeUnit.SECONDS);
        assertThat(recordedRequest).isNotNull();

        assertAll(
                () -> assertThat(recordedRequest.getMethod()).isEqualTo("GET"),
                () -> assertThat(recordedRequest.getPath()).isEqualTo("/math/add/40/2"),
                () -> assertThat(recordedRequest.getBodySize()).isZero()
        );
    }
}

In this test, we first record the response (or responses) we want to receive by calling enqueue with a MockResponse. Don't let the "Mock" in the name fool you, though, since this just tells MockWebServer the response you want. It will take care of returning a real HTTP response from a real HTTP server. The next line in the test is the same as in the Dropwizard example above, where we call the HTTP client and assert the result. But after that, MockWebServer lets you get the requests that the client code made using takeRequest, so you can verify that it sent exactly what it should have, with the expected path, query parameters, headers, body, etc.

One advantage of using MockWebServer is that it is really easy to record different responses and test how your client responds. For example, suppose the Math API returns a 400 response if you provide two numbers that add up to a number higher than the maximum value of a Java int, or a 500 response if there is a server error. Here are a few tests for those situations:

@Test
void shouldThrowIllegalArgumentException_ForInvalidInput() throws InterruptedException {
    server.enqueue(new MockResponse()
            .setResponseCode(400)
            .setHeader(HttpHeaders.CONTENT_TYPE, "text/plain")
            .setBody("overflow"));

    assertThatIllegalArgumentException()
            .isThrownBy(() -> mathClient.add(Integer.MAX_VALUE, 1))
            .withMessage("Invalid arguments: overflow");

    var recordedRequest = server.takeRequest(1, TimeUnit.SECONDS);
    assertThat(recordedRequest).isNotNull();

    assertAll(
            () -> assertThat(recordedRequest.getMethod()).isEqualTo("GET"),
            () -> assertThat(recordedRequest.getPath()).isEqualTo("/math/add/%d/1", Integer.MAX_VALUE)
    );
}

@Test
void shouldThrowIllegalStateException_ForServerError() throws InterruptedException {
    server.enqueue(new MockResponse()
            .setResponseCode(500)
            .setHeader(HttpHeaders.CONTENT_TYPE, "text/plain")
            .setBody("Server error: can't add right now"));

    assertThatIllegalStateException()
            .isThrownBy(() -> mathClient.add(2, 2))
            .withMessage("Unknown error: Server error: can't add right now");

    var recordedRequest = server.takeRequest(1, TimeUnit.SECONDS);
    assertThat(recordedRequest).isNotNull();

    assertAll(
            () -> assertThat(recordedRequest.getMethod()).isEqualTo("GET"),
            () -> assertThat(recordedRequest.getPath()).isEqualTo("/math/add/2/2", Integer.MAX_VALUE)
    );
}

Each test defines the response(s) that the MockWebServer should sent it. This makes it possible to create clean, self-contained test code that is easy to understand and change.

To make these tests pass, we should update the original implementation with some error handling code:

public int add(int a, int b) {
    var response = client.target(baseUri)
            .path("/math/add/{a}/{b}")
            .resolveTemplate("a", a)
            .resolveTemplate("b", b)
            .request()
            .get();

    if (successful(response)) {
        return response.readEntity(Integer.class);
    } else if (clientError(response)) {
        throw new IllegalArgumentException("Invalid arguments: " + response.readEntity(String.class));
    }

    throw new IllegalStateException("Unknown error: " + response.readEntity(String.class));
}

The code examples (adding two numbers) I've used are simple. In "real life" you are probably calling more complicated and expansive APIs, and need to test various success and failure scenarios. To recap, some of the advantages of using MockWebServer in your HTTP client tests are:

  • You can record different responses for each test (similar to setting up mock objects, e.g., Mockito)
  • You can avoid having to implement "stub" resources that are a "shadow API" of the remote API
  • Avoiding complexity in "stub" resources when adding logic to provide different responses based on inputs or other signals
  • You can verify the requests that were made, like how you verify method calls with mocking (e.g., Mockito)

There are other things you can do with MockWebServer, for example you can throttle responses to simulate a slow network to test timeout and retry behavior. You can also test with and without HTTPS, requiring client certificates, and customizing the supported protocols. These are all things that can be done in custom code, but it's much nicer when it comes out of the box.

To sum up, MockWebServer makes it simple to write tests for HTTP client code, allowing you to test the "happy path" and various failure scenarios, and provides support for more advanced testing situations such as when requiring client certificate authentication or simulating network slowness.

JUnit Pioneer Presentation Slides

Posted on March 05, 2021 by Scott Leberknight

Recently I've been using JUnit Pioneer, which is an extension library for JUnit Jupiter (JUnit 5). It contains a lot of useful annotations that are really easy to use in tests, for example to generate a range of numbers for input into a parameterized test. This is a presentation about Pioneer that I gave on March 4, 2021.

JUnit Pioneer from Scott Leberknight

In case the embedded slideshow doesn’t work properly here is a link to the slides (opens in a new window/tab).

Unit Testing Presentation Slides

Posted on July 10, 2019 by Scott Leberknight

We have several interns this summer, and each Friday we're doing a short presentation on a different software development topic. On June 28, I gave a short presentation on (unit) testing. This presentation is very light on code, and heavier on philosophy. I shared the slides on SlideShare and have embedded them below.

Unit Testing from Scott Leberknight

In case the embedded slideshow doesn’t work properly here is a link to the slides (opens in a new window/tab).

JUnit 5 Presentation Slides

Posted on August 13, 2018 by Scott Leberknight

I just gave a short presentation on JUnit 5 at my company, Fortitude Technologies. JUnit 5 adds a bunch of useful features for developer testing such as parameterized tests, a more flexible extension model, and a lot more. Plus, it aims to provide a more clean separation between the testing platform that IDEs and build tools like Maven and Gradle use, versus the developer testing APIs. It also provides an easy migration path from JUnit 4 (or earlier) by letting you run JUnit 3, 4, and 5 tests in the same propject. Here are the slides:

JUnit 5 from Scott Leberknight

In case the embedded slide show does not display properly, here is a link to the slides on Slideshare. The sample code for the presentation is on GitHub here.

Testing HTTP Clients Using Spark, Revisited

Posted on March 14, 2017 by Scott Leberknight

In a previous post I described the very small sparkjava-testing library I created to make it really simple to test HTTP client code using the Spark micro-framework. It is basically one simple JUnit 4 rule (SparkServerRule) that spins up a Spark HTTP server before tests run, and shuts it down once tests have executed. It can be used either as a @ClassRule or as a @Rule. Using @ClassRule is normally what you want to do, which starts an HTTP server before any tests has run, and shuts it down afer all tests have finished.

In that post I mentioned that I needed to do an "incredibly awful hack" to reset the Spark HTTP server to non-secure mode so that, if tests run securely using a test keystore, other tests can also run either non-secure or secure, possibly with a different keystore. I also said the reason I did that was because "there is no way I found to easily reset security". The reason for all that nonsense was because I was using the static methods on the Spark class such as port, secure, get, post, and so on. Using the static methods also implies only one server instance across all tests, which is also not so great.

Well, it turns out I didn't really dig deep enough into Spark's features, because there is a really simple way to spin up separate and independent Spark server instances. You simply use the Service#ignite method to return an instance of Service. You then configure the Service however you want, e.g. change the port, add routes, filters, set the server to run securely, etc. Here's an example:

Service http = Service.ignite();
http.port(56789);
http.get("/hello", (req, resp) -> "Hello, Spark service!");

So now you can create as many servers as you want. This is exactly what is needed for the SparkServerRule, which has been refactored to use Spark#ignite to get separate servers for each test. It now has only one constructor which takes a ServiceInitializer and can be used to do whatever configuration you need, add routes, filters, etc. Since ServiceInitializer is a @FunctionalInterface you can simply supply a lambda expression, which makes it cleaner. Here is a simple example:

@ClassRule
public static final SparkServerRule SPARK_SERVER = new SparkServerRule(http -> {
    http.get("/ping", (request, response) -> "pong");
    http.get("/health", (request, response) -> "healthy");
});

This is a rule that, before any test is run, spins up a Spark server on the default port 4567 with two GET routes, and shuts the server down after all tests have completed. To do things like change the port and IP address in addition to adding routes, you just call the appropriate methods on the Service instance (in the example above, the http object passed to the lambda). Here's an example:

@ClassRule
public static final SparkServerRule SPARK_SERVER = new SparkServerRule(https -> {
    https.ipAddress("127.0.0.1");
    https.port(56789);
    URL resource = Resources.getResource("sample-keystore.jks");
    https.secure(resource.getFile(), "password", null, null);
    https.get("/ping", (request, response) -> "pong");
    https.get("/health", (request, response) -> "healthy");
});

In this example, tests will be able to access a server with two secure (https) endpoints at IP 127.0.0.1 on port 56789. So that's it. On the off chance someone was actually using this rule other than me, the migration path is really simple. You just need to configure the Service instance passed in the SparkServerRule constructor as shown above. Now, each server is totally independent which allows tests to run in parallel (assuming they're on different ports). And better, I was able to remove the hack where I used reflection to go under the covers of Spark and manipulate fields, etc. So, test away on that HTTP client code!

This blog was originally published on the Fortitude Technologies blog here.

Testing HTTP Clients Using the Spark Micro Framework

Posted on December 07, 2016 by Scott Leberknight

Testing HTTP client code can be a hassle. Your tests either need to run against a live HTTP server, or you somehow need to figure out how to send mock requests which is generally not easy in most libraries that I have used. The tests should also be fast, meaning you need a lightweight server that starts and stops quickly. Spinning up heavyweight web or application servers, or relying on a specialized test server, is generally error-prone, adds complexity and slows tests down. In projects I'm working on lately we are using Dropwizard, which provides first class testing support for testing JAX-RS resources and clients as JUnit rules. For example, it provides DropwizardClientRule, a JUnit rule that lets you implement JAX-RS resources as test doubles and starts and stops a simple Dropwizard application containing those resources. This works great if you are already using Dropwizard, but if not then a great alternative is Spark. Even if you are using Dropwizard, Spark can still work well as a test HTTP server.

Spark is self-described as a "micro framework for creating web applications in Java 8 with minimal effort". You can create the steroptypical "Hello World" in Spark like this (shamelessly copied from Spark's web site):

import static spark.Spark.get;

public class HelloWorld {
    public static void main(String[] args) {
        get("/hello", (req, res) -> "Hello World");
    }
}

You can run this code and visit http://localhost:4567 in a browser or using a client tool like curl or httpie. Spark is a perfect fit for creating HTTP servers in tests (whether you call them unit tests, integration tests or something else is up to you, I will just call them tests here). I have created a very simple library sparkjava-testing that contains a JUnit rule for spinning up a Spark server for functional testing of HTTP clients. This library consists of one JUnit rule, the SparkServerRule. You can annotate this rule with @ClassRule or just @Rule. Using @ClassRule will start a Spark server one time before any test is run. Then your tests run, making requests to the HTTP server, and finally once all tests have finished the server is shut down. If you need true isolation between every single test, annotate the rule with @Rule and a test Spark server will be started before each test and shut down after each test, meaning each test runs against a fresh server. (The SparkServerRule is a JUnit 4 rule mainly because JUnit 5 is still in milestone releases, and because I have not actually used JUnit 5.)

To declare a class rule with a test Spark server with two endpoints, you can do this:

@ClassRule
public static final SparkServerRule SPARK_SERVER = new SparkServerRule(() -> {
    get("/ping", (request, response) -> "pong");
    get("/healthcheck", (request, response) -> "healthy");
});

The SparkServerRule constructor takes a Runnable which define the routes the server should respond to. In this example there are two HTTP GET routes, /ping and /healthcheck. You can of course implement the other HTTP verbs such as POST and PUT. You can then write tests using whatever client library you want. Here is an example test using a JAX-RS:

@Test
public void testSparkServerRule_HealthcheckRequest() {
    client = ClientBuilder.newBuilder().build();
    Response response = client.target(URI.create("http://localhost:4567/healthcheck"))
            .request()
            .get();
    assertThat(response.getStatus()).isEqualTo(200);
    assertThat(response.readEntity(String.class)).isEqualTo("healthy");
}

In the above test, client is a JAX-RS Client instance (it is an instance variable which is closed after each test). I'm using AssertJ assertions in this test. The main thing to note is that your client code be parameterizable, so that the local Spark server URI can be injected instead of the actual production URI. When using the JAX-RS client as in this example, this means you need to be able to supply the test server URI to the Client#target method. Spark runs on port 4567 by default, so the client in the test uses that port.

The SparkServerRule has two other constructors: one that accepts a port in addition to the routes, and another that takes a SparkInitializer. To start the test server on a different port, you can do this:

@ClassRule
public static final SparkServerRule SPARK_SERVER = new SparkServerRule(6543, () -> {
    get("/ping", (request, response) -> "pong");
    get("/healthcheck", (request, response) -> "healthy");
});

You can use the constuctor that takes a SparkInitializer to customize the Spark server, for example in addition to changing the port you can also set the IP address and make the server secure. The SparkInitializer is an @FunctionalInterface with one method init(), so you can use a lambda expression. For example:

@ClassRule
public static final SparkServerRule SPARK_SERVER = new SparkServerRule(
        () -> {
            Spark.ipAddress("127.0.0.1");
            Spark.port(9876);
            URL resource = Resources.getResource("sample-keystore.jks");
            String file = resource.getFile();
            Spark.secure(file, "password", null, null);
        },
        () -> {
            get("/ping", (request, response) -> "pong");
            get("/healthcheck", (request, response) -> "healthy");
        });

The first argument is the initializer. It sets the IP address and port, and then loads a sample keystore and calls the Spark#secure method to make the test sever accept HTTPS connections using a sample keystore. You might want to customize settings if running tests in parallel, specifically the port, to ensure parallel tests do not encounter port conflicts.

The last thing to note is that SparkServerRule resets the port, IP address, and secure settings to the default values (4567, 0.0.0.0, and non-secure, respectively) when it shuts down the Spark server. If you use the SparkInitializer to customize other settings (for example the server thread pool, static file location, before/after filters, etc.) those will not be reset, as they are not currently supported by SparkServerRule. Last, resetting to non-secure mode required an incredibly awful hack because there is no way I found to easily reset security - you cannot just pass in a bunch of null values to the Spark#secure method as it will throw an exception, and there is no unsecure method probably because the server was not intended to set and reset things a bunch of times like we want to do in test scenarios. If you're interested, go look at the code for the SparkServerRule in the sparkjava-testing repository, but prepare thyself and get some cleaning supplies ready to wash away the dirty feeling you're sure to have after seeing it.

The ability to use SparkServerRule to quickly and easily setup test HTTP servers, along with the ability to customize the port, IP address, and run securely intests has worked very well for my testing needs thus far. Note that unlike the above toy examples, you can implement more complicated logic in the routes, for example to return a 200 or a 404 for a GET request depending on a path parameter or request parameter value. But at the same time, don't implement extremely complex logic either. Most times I simply create separate routes when I need the test server to behave differently, for example to test various error conditions. Or, I might even choose to implement separate JUnit test classes for different server endpoints, so that each test focuses on only one endpoint and its various success and failure conditions. As is many times the case, the context will determine the best way to implement your tests. Happy testing!

This blog was originally published on the Fortitude Technologies blog here.