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)',
+                                               },
                                        ],
                                },
                                {

Reply via email to