Mock server for client tests

conta-http-mock-server stands up a real HTTP server whose every route is answered by a test-supplied callback. Point the client-under-test at it, script the responses, and assert the request the client actually sent.

It is the response side of testing a declarative @Client (or any HttpClient): no WireMock, no hand-rolled EmbeddedServer wiring.

Installation

A test-support module — add it to testImplementation (see Getting started for the repository):

dependencies {
    testImplementation 'no.conta.http:conta-http-mock-server:0.+'
}

It brings Micronaut’s Netty server transitively. It does not bring a JSON mapper or Spock/Groovy — those come from your own test classpath: any Micronaut app test already has a JsonMapper (jackson-databind or serde), and that is what the mock server uses to bind and serialize bodies. A test module with no mapper at all must add one (e.g. micronaut-jackson-databind).

How it works

MockController is an env-gated catch-all controller: under the mock-http-server environment it maps GET/POST/PUT/DELETE on every path and delegates each request to an injected MockHttpHandler:

HttpResponse<?> handle(HttpMethod method, HttpRequest<?> request);

The verb is passed separately so interactions can branch on it (1 * handler.handle(HttpMethod.POST, _)); everything else — path, query, headers, cookies, body — is on the HttpRequest. In a test the handler is a Spock Mock(): its >> return becomes the response the client receives, and its interaction count/arguments are your assertion on the outbound request. Pull the body and params out with MockHttpRequests (or the Groovy request.extractBodyAsMap() / request.extractQueryParams() extensions).

Spock: extend MockServerSpec

The base spec supplies a Mock() handler and a MockServer started against it, fresh for each feature method. A declarative @Client resolves its base URL from configuration, so point its service id at mockUrl() in a context that holds the client:

@Client(id = 'widgets')
interface WidgetClient {
    @Get('/widgets/{id}')
    Map<String, Object> get(String id)
}

class WidgetClientTest extends MockServerSpec {

    @AutoCleanup
    ApplicationContext clientContext = ApplicationContext.run(
        ['micronaut.http.services.widgets.url': mockUrl().toString()], Environment.TEST)

    @Subject
    WidgetClient client = clientContext.getBean(WidgetClient)

    def 'fetches a widget'() {
        when:
            def widget = client.get('42')
        then:
            1 * handler.handle(HttpMethod.GET, _) >> HttpResponse.ok([id: '42', name: 'Anvil'])
        and:
            widget.name == 'Anvil'
    }
}

To assert what the client sent — auth headers, the request body — read it off the request inside the response closure:

1 * handler.handle(HttpMethod.POST, _) >> { method, request ->
    assert request.headers.get('Authorization') == 'Bearer t0ken'
    assert request.extractBodyAsMap() == [sku: 'A1', qty: 2]   // or MockHttpRequests.extractBodyAsMap(request)
    HttpResponse.ok([id: '99'])
}

For a one-off request you do not need a declarative client at all — a low-level HttpClient.create(mockUrl().toURL()) pointed straight at the server is enough.

Override environments() to activate extra Micronaut environments alongside test and mock-server:

@Override
protected String[] environments() {
    ['api'] as String[]
}

Plain: use MockServer directly

MockServer is AutoCloseable and framework-agnostic — use it from JUnit, try-with-resources, or a @Shared Spock field when you want one server for the whole spec rather than one per feature:

var handler = mock(MockHttpHandler.class);
try (var server = MockServer.start(handler);
     var client = HttpClient.create(server.url().toURL())) {
    var response = client.toBlocking().exchange(HttpRequest.GET("/widgets/42"), Map.class);
    // exercise client, assert on handler
}

start(handler, "api") adds environments; url(), context(), and server() expose what you need to wire a client; close() stops the server. A declarative @Client is wired exactly as in the Spock example — set micronaut.http.services.<id>.url to server.url() in the context that holds the client.

Limitations

The server is intentionally thin; for v1:

  • extractBodyAsMap only decodes a body that is a JSON object. Array, string, and numeric bodies return null — read them off the request directly if you need them.

  • The handler is typically a Spock Mock(), which is not thread-safe — drive the client sequentially. Concurrent in-flight requests against one server can corrupt interaction counts.