This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/docs by this push:
new 7ff19c83f4 feat: @MicroserviceTest JUnit 5 extension for
whole-microservice integration tests
7ff19c83f4 is described below
commit 7ff19c83f4f143d6b5748bea6bc504004f513d89
Author: James Bognar <[email protected]>
AuthorDate: Fri Jun 19 09:17:50 2026 -0400
feat: @MicroserviceTest JUnit 5 extension for whole-microservice
integration tests
---
pages/release-notes/10.0.0.md | 15 +++++
pages/topics/15.10.MicroserviceTesting.md | 93 +++++++++++++++++++++++++++++++
sidebars.ts | 5 ++
3 files changed, 113 insertions(+)
diff --git a/pages/release-notes/10.0.0.md b/pages/release-notes/10.0.0.md
index 5d387223ba..e9408bc399 100644
--- a/pages/release-notes/10.0.0.md
+++ b/pages/release-notes/10.0.0.md
@@ -155,6 +155,21 @@ JettySettings jettySettings() {
See the new [Graceful Shutdown & Readiness
Gating](/docs/topics/GracefulShutdown) topic page for details, including the
Jetty/Tomcat parity table and Kubernetes deployment guidance.
+### juneau-microservice-test (new module)
+
+#### `@MicroserviceTest` — whole-microservice JUnit 5 integration tests
+
+A new `juneau-microservice-test` module adds `@MicroserviceTest`, a JUnit 5
extension that boots a whole `Microservice` (config + lifecycle + embedded
Jetty on an ephemeral port) for a test class — the standalone-microservice
analog of Spring's `@SpringBootTest`.
+
+- **Explicit SUT.** Declare the system under test via
`@MicroserviceTest(configurations={...})` and/or a `static Microservice.Builder
microserviceBuilder()` supplier method (no classpath scanning). The extension
adds an ephemeral-Jetty `Server` (port 0) + `JettyConfiguration` automatically;
Jetty for v1 (Tomcat pluggability is a follow-on).
+- **Composes the existing `@TestBean` substrate** (no parallel injection
annotation). Default **Mode INJECT** installs overrides via
`Microservice.Builder.overridingBeanStore(...)` *before* boot, so the service
reads them from startup; **Mode OVERLAY** pushes/pops against the booted
instance.
+- **Parameter resolution** for `RestClient` (bound to the booted root URL —
the primary convenience), `Microservice`, `WritableBeanStore`, and the bound
port.
+- **Fresh instance per class**, stopped cleanly in teardown (restart is
unsupported); `stop()` restores process globals so multiple `@MicroserviceTest`
classes are isolated within a JVM. The bound port is always read from the live
`ServerConnector`.
+- Distinct from `MockRestClient`: use `@MicroserviceTest` for a real
server/lifecycle over HTTP, `MockRestClient` for in-JVM single-resource tests.
+- This also added a small public composition entry point —
`JuneauBeanStoreExtension.discoverOverrides(...)` — so other JUnit 5 extensions
can reuse the `@TestBean` discovery without driving that extension's own
lifecycle.
+
+See the new [Whole-Microservice Integration Tests
(@MicroserviceTest)](/docs/topics/MicroserviceTesting) topic page.
+
### Build & packaging (BOM + curated bundles)
#### Published BOM + curated dependency bundles
diff --git a/pages/topics/15.10.MicroserviceTesting.md
b/pages/topics/15.10.MicroserviceTesting.md
new file mode 100644
index 0000000000..0fd437174f
--- /dev/null
+++ b/pages/topics/15.10.MicroserviceTesting.md
@@ -0,0 +1,93 @@
+---
+title: "Whole-Microservice Integration Tests (@MicroserviceTest)"
+slug: MicroserviceTesting
+---
+
+`@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](/docs/topics/RestServerTestBeanInjection) for collaborator
substitution.
+
+It ships in a small dedicated support module:
+
+```xml
+<dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-microservice-test</artifactId>
+ <version>10.0.0</version>
+ <scope>test</scope>
+</dependency>
+```
+
+## When to use it (vs. `MockRestClient`)
+
+The two integration-test paths are intentionally distinct:
+
+| Use | For |
+| --- | --- |
+| **`@MicroserviceTest`** | A genuine full-microservice test — real server,
real connectors, real lifecycle, exercised over HTTP. |
+| **`MockRestClient`** | An 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. v1 boots Jetty; Tomcat pluggability is a follow-on.
+
+```java
+@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`](/docs/topics/RestServerTestBeanInjection) (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).
+
+```java
+@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 `@Micros [...]
+
+The bound port is always read from the live `ServerConnector` (never
hard-coded), so ephemeral-port binding never collides.
diff --git a/sidebars.ts b/sidebars.ts
index 3cedd89034..bfe4e2e473 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -2037,6 +2037,11 @@ const sidebars: SidebarsConfig = {
id:
'topics/15.09.InjectAwareMicroservice',
label: '15.9.
Inject-Aware Microservice',
},
+ {
+ type: 'doc',
+ id:
'topics/15.10.MicroserviceTesting',
+ label: '15.10.
Whole-Microservice Integration Tests (@MicroserviceTest)',
+ },
],
},
{