All Articles

Using MockWebServer with Spring Boot and Spock

Integration tests are easier when you use a mock server. There’s a lot of mock servers available out there. The most popular one that I know of and I’ve worked with is WireMock. It does a lot of things correctly and I ‘d be comfortable in recommending it every time you need a mock server. An added bonus is that it has an integration with Spring Boot.

If you’re looking for something else to consider, I’ve stumbled upon MockWebServer recently. Its simplicity is very surprising.

Using MockWebServer

I had to implement a mock server to replace the server stubs used in the integration tests of one of our services. Someone suggested MockWebServer based on its usage in Spring codebase. I went ahead and used it.

We can use MockWebServer as is or we can use a library from fabric8 that wraps MockWebServer and gives us a nice DSL. Or do as I’ve done and create our own simple wrapper around MockWebServer.

We will be using Spring Boot and Spock for this example. We’ll be using RestTemplate to call external services. Although, you should start looking at WebClient instead.

Simple DSL

If you haven’t read the documentation of MockWebServer, please go ahead and read it now. It’s very simple and straightforward.

We’ll try to emulate a smaller scope of what fabric8 created. The server will respond as long as there is a request. And the DSL will be server.expect().withPath().andReturn().

We will be using Spock as our test framework. Thus, the following (test) code will all be using Groovy.

server

Of course, we will always start with the mock server itself. As such we’ll create a class that wraps MockWebserver:

class DefaultMockServer {
    private final MockWebServer server
}

It does not look like it will be useful to us at this point so let’s get on with the next part.

expect()

Our main goal for using a mock server is to get responses from mocked endpoints. For our purpose we will call these endpoints as expectations. We can model these as MockServerExpectation.

Since our server will host this expectations we can create expect in DefaultMockServer. It will return an expectation that we can manipulate later on:

class DefaultMockServer {
    ...
    private final List<MockServerExpectation> expectations = []

    MockServerExpectation expect() {
        def expectation = new MockServerExpectation()
        this.expectations.add(expectation)
        return expectation
    }
}

class MockServerExpectation {}

withPath()

Now that we are able to create expectations we can now configure them. Let’s start with the path of the endpoint:

class MockServerExpectation {
    String path

    MockServerExpectation withPath(String path) {
        this.path = path
        return this
    }
}

Nice! But this isn’t still useful if we are not returning anything. Let’s take care of that.

andReturn()

With MockWebServer, we will return a MockResponse that a Dispatcher will send back.

That’s a lot of concepts to take in. Let’s focus on configuring the response first. All we need is a status and an object that we will transform into JSON.

class MockServerExpectation {
    ...
    MockResponse response
    
    ...
    MockServerExpectation andReturn(int statusCode, Object response) {
        this.response = new MockResponse().setResponseCode(statusCode)
            .setBody(JsonOutput.toJson(response))
            .addHeader("Content-Type", "application/json; charset=utf-8")
            .addHeader("Cache-Control", "no-cache")
        return this
    }
}

We are now able to configure an endpoint’s main parts: path and response.

We need to have a way for our mock server to know which expectation will respond to an incoming request. That is the role of a Dispatcher.

Dispatcher

We’ve setup our endpoints and now when we start our server it should start listening to any request and return an appropriate response.

With MockWebServer, the one responsible in relaying the request to the correct response is the Dispatcher. The Dispatcher will take a RecordedRequest and then return a MockResponse based from that.

Let’s implement that and also take care of requests for endpoints that don’t exist:

class DefaultMockServer {
    private final MockWebServer server = new MockWebServer()

    ...

    void start(int port) {
        server.setDispatcher(new Dispatcher() {
            @Override
            MockResponse dispatch(@NotNull RecordedRequest recordedRequest) throws InterruptedException {
                return new MockResponse().setResponseCode(404)
            }
        })
        server.start(port)
    }
}

We can use DefaultMockServer in its current state and it will always return the same 404 response. Since we will be calling two different external services, we need two different instances of MockWebServer listening on different ports. Hence, the port argument of start.

To make it more useful we can use RecordedRequest to dispatch the request to the correct MockServerExpectation. To make our code more organised and simpler we will create our own request object called MockServerRequest:

class MockServerRequest {
    String path

    MockServerRequest(RecordedRequest recordedRequest) {
        this.path = recordedRequest.path
    }

    boolean matches(String path) {
        return this.path.matches(path)
    }
}

class DefaultMockServer {
    ...
    private final List<MockServerExpectation> expectations = []

    ...
    void start(int port) {
        server.setDispatcher(new Dispatcher() {
            @Override
            MockResponse dispatch(@NotNull RecordedRequest recordedRequest) throws InterruptedException {
                def mockRequest = new MockServerRequest(recordedRequest)
                def expectation = expectations.find { mockRequest.matches(it.path) }

                return expectation ? expectation.response : new MockResponse().setResponseCode(404)
            }
        })
        server.start(port)
    }
}

Our current implementation is very simplistic. We can extend this to have its own representation of the query parameters and request body if we like. I’ve been able to do it, but I’ll leave it for you to try.

Integration Test

Our MockWebServer wrapper is now ready to be used. Note that the ports we are using are hardcoded in the services so make sure you are using the correct ports.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class CheckoutControllerPurchaseSpec extends Specification {
    @Autowired
    CheckoutController controller

    List<DefaultMockServer> servers = []

    def setup() {
        setupCartService()
        setupPurchaseService()
    }

    def "should be successful checkout"() {
        when:
        def response = controller.purchase(123, 234)

        then:
        response.statusCode.value() == 200
        response.getBody() instanceof CheckoutResponse
        CheckoutResponse checkoutResponse = (CheckoutResponse) response.body
        checkoutResponse.amount == 100
        checkoutResponse.successful
    }

    void setupCartService() {
        DefaultMockServer server = new DefaultMockServer()

        server.expect().withPath("/cart/\\d+/total")
            .andReturn(200, new CartTotal(1L, BigDecimal.valueOf(100)))

        server.start(5001)
        servers.add(server)
    }

    void setupPurchaseService() {
        DefaultMockServer server = new DefaultMockServer()

        server.expect().withPath("/purchase/\\d+/\\d+")
                .andReturn(200, new PurchaseAttempt(1, true))

        server.start(5002)
        servers.add(server)
    }

    def cleanup() {
        servers.each { it.shutdown() }
    }
}

Conclusion

We’ve created a very basic DSL wrapper of MockWebServer which we can easily extend to handle more complex use cases. This is all possible because it is very simple and lightweight.

It’s good that we can rely on tools we’re familiar with, but we should never forget to try other tools that might just be enough for our use case.