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:
-
extractBodyAsMaponly decodes a body that is a JSON object. Array, string, and numeric bodies returnnull— 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.