Skip to main content

Whole-Microservice Integration Tests (@MicroserviceTest)

@MicroserviceTest is a JUnit 5 extension that boots a whole Microservice — config, lifecycle, and an embedded Jetty server on an ephemeral port — for the duration of a test class, then stops it cleanly. It's the standalone-microservice analog of Spring's @SpringBootTest, and it composes the existing @TestBean mock-bean substrate for collaborator substitution.

It ships in a small dedicated support module:

<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-microservice-test</artifactId>
<version>${juneau.version}</version>
<scope>test</scope>
</dependency>

When to use it (vs. MockRestClient)​

The two integration-test paths are intentionally distinct:

UseFor
@MicroserviceTestA genuine full-microservice test — real server, real connectors, real lifecycle, exercised over HTTP.
MockRestClientAn in-JVM test of a single @Rest resource — no server, no sockets, dispatched directly against a RestContext.

MockRestClient does not talk to a booted server; reach for @MicroserviceTest when you want the actual server, filters, and connector path in the loop.

Specifying the system under test​

The SUT is declared explicitly (no classpath scanning), two complementary ways:

  1. configurations={...} — one or more @Configuration classes whose @Bean Servlet methods the microservice auto-mounts (the common case).
  2. A static Microservice.Builder supplier method on the test class (default name microserviceBuilder, override via builderMethod) — for full control of the builder. Any configurations={...} are appended to whatever the supplier returns.

Either way the extension additionally installs an ephemeral-Jetty Server (bound to port 0) and JettyConfiguration, so tests only contribute their resources. The extension currently boots Jetty.

@MicroserviceTest(configurations = MyServiceTest.AppConfig.class)
class MyServiceTest {

@Configuration
public static class AppConfig {
@Bean Greeter greeter() { return new ProductionGreeter(); }
@Bean Servlet greetingResource(Greeter greeter) { return new GreetingResource(greeter); }
}

@Test
void greetingEndpoint(RestClient client) throws Exception {
var resp = client.get("/greeting").run();
assertEquals(200, resp.getStatusCode());
}
}

What gets injected into tests​

Test methods (and lifecycle methods) may declare parameters the extension resolves:

  • RestClient — a real HTTP client bound to the booted server's root URL (the primary convenience).
  • Microservice — the booted instance.
  • WritableBeanStore — the booted instance's bean store.
  • int / Integer — the bound (ephemeral) port.

Mock-bean injection​

Substitute collaborators with the existing @TestBean (the @MockBean analog) — there is no parallel injection annotation. The default is Mode INJECT: the override is installed via Microservice.Builder.overridingBeanStore(...) before boot, so the service reads it from startup (the correct mode for whole-service tests, since the microservice resolves its beans at boot).

@MicroserviceTest(configurations = AppConfig.class)
class MyServiceTest {

// Substituted before the microservice boots — the real GreetingResource sees this mock.
@TestBean(scope = Scope.CLASS)
static Greeter mockGreeter() {
return () -> "mocked";
}

@Test
void usesTheMock(RestClient client) throws Exception {
assertEquals("mocked", client.get("/greeting").run().getBodyAsString());
}
}

@TestBean(mode = Mode.OVERLAY) instead pushes the overrides onto the already-booted instance's bean store (and pops them in teardown) — the opt-in path for per-test substitution against a long-running instance. Note that OVERLAY only affects beans the framework re-resolves through the bean store per call; beans pinned into per-op memoizers at boot need INJECT.

Lifecycle and isolation​

The microservice is booted once per test class (beforeAll) and stopped in afterAll — a fresh instance per class, never reused. Microservice restart is unsupported (its stopped flag is one-way, the bean store is closed on stop, and each start() registers a JVM shutdown hook), so the build→start→stop cycle is repeated per class. stop() also restores the process globals the microservice sets (getInstance(), Config system default, Settings sources), so multiple @MicroserviceTest classes in one JVM stay isolated. Do not boot microservices in parallel within a JVM — they would stomp those globals.

The bound port is always read from the live ServerConnector (never hard-coded), so ephemeral-port binding never collides.