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 d123ca9079 docs: TODO-35 BeanStore test injection topic + 9.5 release 
notes + sidebar
d123ca9079 is described below

commit d123ca907961128f788756867ed24826a9d24ff7
Author: James Bognar <[email protected]>
AuthorDate: Sat May 23 12:47:52 2026 -0400

    docs: TODO-35 BeanStore test injection topic + 9.5 release notes + sidebar
    
    New topic 10.20d.RestServerTestBeanInjection covers the 
JuneauBeanStoreExtension,
    @TestBean (mode/scope/name), TestBeanStore overlay builder, Mode INJECT
    (construction-time wiring) vs Mode OVERLAY (push/pop on a live SUT), and the
    BeanStoreOverridable<B> surface across MockRestClient, Microservice,
    SerializerSet, ParserSet, and EncoderSet. Sidebar registers the new page;
    9.5.0 release notes add a TODO-35 entry under juneau-rest-server.
    
    Co-authored-by: Cursor <[email protected]>
---
 pages/release-notes/9.5.0.md                       |  13 +
 pages/topics/10.20d.RestServerTestBeanInjection.md | 268 +++++++++++++++++++++
 sidebars.ts                                        |   5 +
 3 files changed, 286 insertions(+)

diff --git a/pages/release-notes/9.5.0.md b/pages/release-notes/9.5.0.md
index 749c678b8a..5d51815214 100644
--- a/pages/release-notes/9.5.0.md
+++ b/pages/release-notes/9.5.0.md
@@ -1986,6 +1986,19 @@ String name
 
 ### juneau-rest-server
 
+#### Test-time Bean Injection (TODO-35)
+
+A new JUnit 5 extension and `@TestBean` annotation enable Spring-style 
test-time bean substitution for `MockRestClient`, `Microservice`, 
`SerializerSet`, `ParserSet`, and `EncoderSet` — without CGLIB, AOT bytecode 
generation, or any Mockito dependency. See [REST Server — Test-time Bean 
Injection](/docs/topics/RestServerTestBeanInjection) for the full reference.
+
+- New module **`juneau-junit5`** with `JuneauBeanStoreExtension`, 
`@TestBean(name, type, scope, mode)`, `Scope`, `Mode`, and `TestBeanStore` (a 
fluent `BasicBeanStore` overlay-builder).
+- New `BeanStoreOverridable<B>` marker interface in `juneau-commons` plus 
`overridingBeanStore(BeanStore)` setters on `MockRestClient.Builder`, 
`Microservice.Builder`, `SerializerSet.Builder`, `ParserSet.Builder`, and 
`EncoderSet.Builder`. The overlay is installed in the `overridingParent` slot 
of the builder's bean store, putting overrides at tier 1 of the resolution 
chain.
+- New `Args.overridingParent` slot on `RestContext.Args` for threading the 
overlay into the bean store the `RestContext` constructs internally.
+- **Two wiring modes:**
+  - **Mode INJECT** (default) — construction-time wiring with overrides. The 
overlay is layered into the SUT's bean store at construction time. Universal — 
every bean type is eligible for replacement.
+  - **Mode OVERLAY** (opt-in via `@TestBean(mode = Mode.OVERLAY)`) — push/pop 
overlays on a live SUT. The extension is `attach(...)`-ed to a long-lived SUT's 
bean store; per-test overlays are push'd at `@BeforeEach` / `@BeforeAll` and 
pop'd at `@AfterEach` / `@AfterAll`. Backed by new 
`WritableBeanStore.pushOverlay(BeanStore)` / `popOverlay(Snapshot)` primitives.
+- **Per-test and per-class scopes** via `@TestBean(scope = METHOD)` (default) 
and `@TestBean(scope = CLASS)` (must be on `static` members). Method-scope 
overlays chain on top of class-scope overlays.
+- **Named-bean qualifier** via `@TestBean(name = "...")` matching the 
framework's existing `@Bean(name = "...")` parameter resolution.
+
 #### Rate-Limit Guard + Request-Id Filter (TODO-66)
 
 `juneau-rest-server` now ships two opt-in operational primitives — both purely 
additive, both wired through existing `@Bean` / `@RestStartCall` extension 
points. See [REST Server — Rate-Limiting and Request-Id 
Propagation](/docs/topics/RestServerRateLimitAndRequestId) for the full 
reference.
diff --git a/pages/topics/10.20d.RestServerTestBeanInjection.md 
b/pages/topics/10.20d.RestServerTestBeanInjection.md
new file mode 100644
index 0000000000..d37a3da3b1
--- /dev/null
+++ b/pages/topics/10.20d.RestServerTestBeanInjection.md
@@ -0,0 +1,268 @@
+---
+title: "Test-time Bean Injection"
+slug: RestServerTestBeanInjection
+---
+
+`juneau-junit5` provides a small JUnit 5 extension for swapping production 
beans with test doubles (mocks/fakes) inside a Juneau `BeanStore` for the 
duration of a JUnit 5 test — without modifying production code and without 
proxying. It's the Juneau-flavored answer to Spring's `@TestConfiguration` / 
`@MockBean`.
+
+The mechanism rests on three pieces:
+
+- A new module — 
[`juneau-junit5`](https://search.maven.org/artifact/org.apache.juneau/juneau-junit5)
 — containing the JUnit 5 extension and a `@TestBean` annotation.
+- A small overlay-builder, `TestBeanStore`, that wraps the existing 
`BasicBeanStore` overriding-parent slot.
+- A consistent `overridingBeanStore(BeanStore)` setter convention (the 
`BeanStoreOverridable<B>` marker interface) on the four user-facing builders 
that Juneau apps construct: `MockRestClient.Builder`, `Microservice.Builder`, 
`SerializerSet.Builder`, `ParserSet.Builder`, and `EncoderSet.Builder`.
+
+Two wiring patterns are supported: **Mode INJECT** (construction-time wiring 
with overrides — the default) and **Mode OVERLAY** (push/pop overlays on a live 
SUT). Mode INJECT covers the vast majority of REST and serializer tests; Mode 
OVERLAY is the opt-in path for tests that hold a long-running `Microservice` 
and want to apply per-test overlays without rebuilding it.
+
+## Mode INJECT — construction-time wiring with overrides
+
+This is the default and what every `@TestBean`-based test ships with out of 
the box. The system under test (SUT) is constructed **after** the overlay is 
built; the overlay is threaded into the builder's `overridingBeanStore(...)` 
setter, which installs it in the `overridingParent` slot of the SUT's bean 
store at construction time.
+
+Because the overlay sits at tier 1 of the bean-store resolution chain (above 
local `@Bean` factories and framework-default memoizers), Mode INJECT is 
**universal**: every bean type is eligible for replacement, including ones the 
framework would otherwise pin into a per-op memoizer at boot.
+
+### Worked example — `MockRestClient` + `@TestBean`
+
+```java
+@ExtendWith(JuneauBeanStoreExtension.class)
+class MyResourceTest {
+
+    @TestBean
+    ExternalApi mockApi = () -> "test-double";
+
+    @Test
+    void who_returnsTheTestDouble(TestBeanStore store) throws Exception {
+        try (var client = MockRestClient.builder(MyResource.class)
+                .overridingBeanStore(store)
+                .build()) {
+            try (var resp = client.get("/who").run()) {
+                assertEquals(200, resp.getStatusCode());
+                assertTrue(resp.getBodyAsString().contains("test-double"));
+            }
+        }
+    }
+
+    // The resource under test — declares a production @Bean ExternalApi
+    // factory; the @TestBean above shadows it for this test.
+    @Rest(path = "/api")
+    public static class MyResource {
+        @Bean public ExternalApi externalApi() { return () -> "production"; }
+        @RestGet("/who") public String who(ExternalApi api) { return 
api.describe(); }
+    }
+
+    public interface ExternalApi {
+        String describe();
+    }
+}
+```
+
+Three things to notice:
+
+1. The extension never reflects on the test class to find `MyResource` / 
`MockRestClient` references. The wiring is explicit — one line: 
`.overridingBeanStore(store)`.
+2. The `TestBeanStore store` parameter is resolved by the extension's 
`ParameterResolver`. The same store can be retrieved via 
`JuneauBeanStoreExtension.getStore()` from `@BeforeAll` / `@BeforeEach` bodies 
if the extension is registered via `@RegisterExtension` instead of 
`@ExtendWith`.
+3. The resource's own `@Bean ExternalApi externalApi()` factory is **not** 
removed — it's just shadowed by the overlay's higher-priority entry. Tests that 
don't install an overlay continue to see the production factory.
+
+### Per-test vs per-class scope
+
+The default scope is `Scope.METHOD`: each `@Test` method gets a fresh overlay 
built from the instance-field / instance-method `@TestBean` declarations. For 
overrides that should be shared across all tests in a class, use `Scope.CLASS` 
on a `static` field or `static` method:
+
+```java
+@ExtendWith(JuneauBeanStoreExtension.class)
+class MySharedOverlayTest {
+
+    @TestBean(scope = Scope.CLASS)
+    static MyExpensiveService sharedSvc = new MyExpensiveService();
+
+    @TestBean
+    PerTestService perTestSvc = new PerTestService(); // rebuilt every method
+
+    @Test void a01_test(TestBeanStore store) { /* sees both overlays */ }
+    @Test void a02_test(TestBeanStore store) { /* still sees sharedSvc; 
perTestSvc is fresh */ }
+}
+```
+
+Method-scope overrides are chained on top of class-scope ones, so a 
method-scope override of `(Type, name)` shadows a class-scope override of the 
same pair, while the class-scope's other entries continue to be visible.
+
+A non-`static` field annotated with `@TestBean(scope = Scope.CLASS)` is 
rejected at `beforeAll` time with a clear error — instance state isn't 
available before any test method runs.
+
+### Named-bean qualifier
+
+`@TestBean(name = "...")` distinguishes two overrides of the same type. This 
matches the framework's existing `@Bean(name = "...")` parameter resolution:
+
+```java
+@TestBean(name = "primary")
+MyService primary = new InMemoryMyService("p");
+
+@TestBean(name = "secondary")
+MyService secondary = new InMemoryMyService("s");
+
+@Test void each_resolves_to_its_qualifier(TestBeanStore store) {
+    assertSame(primary,   store.getBean(MyService.class, 
"primary").orElseThrow());
+    assertSame(secondary, store.getBean(MyService.class, 
"secondary").orElseThrow());
+}
+```
+
+A resource consuming named beans via `@Bean(name = "primary")` constructor 
parameters sees the overlay-supplied values once the SUT is built with 
`.overridingBeanStore(store)`.
+
+### Explicit-type override
+
+The annotation's `type()` member lets a test declare a factory whose return 
type is a supertype (or even `Object`) of the intended registration type:
+
+```java
+@TestBean(type = Greeter.class)
+HelloGreeter typedGreeter = new HelloGreeter();
+
+@Test void d01_typeOverride(TestBeanStore store) {
+    assertSame(typedGreeter, store.getBean(Greeter.class).orElseThrow());
+    assertTrue(store.getBean(HelloGreeter.class).isEmpty()); // registered 
under Greeter only
+}
+```
+
+### Beyond REST — `Microservice`, serializers, parsers, encoders
+
+The same `overridingBeanStore(BeanStore)` setter is available on every Juneau 
builder that exposes a bean store via the `BeanStoreOverridable<B>` interface:
+
+| Builder                          | Setter                              |
+|----------------------------------|-------------------------------------|
+| `MockRestClient.Builder`         | `.overridingBeanStore(BeanStore)`   |
+| `Microservice.Builder`           | `.overridingBeanStore(BeanStore)`   |
+| `SerializerSet.Builder`          | `.overridingBeanStore(BeanStore)`   |
+| `ParserSet.Builder`              | `.overridingBeanStore(BeanStore)`   |
+| `EncoderSet.Builder`             | `.overridingBeanStore(BeanStore)`   |
+
+The wiring is identical across all of them — build a `TestBeanStore`, declare 
`@TestBean` overrides, hand the store to the builder. The extension's 
`getStore()` accessor returns the right store for the current scope 
(method-scope if a `@Test` method is executing, otherwise class-scope).
+
+```java
+@TestBean
+MyDependency dep = mock(MyDependency.class);
+
+@Test void serializer_sees_overlay(TestBeanStore store) {
+    var ser = SerializerSet.create()
+        .overridingBeanStore(store)
+        .add(JsonSerializer.class)
+        .build();
+    // JsonSerializer construction now resolves MyDependency from the overlay.
+}
+```
+
+## Mode OVERLAY — push/pop on a live SUT
+
+Mode OVERLAY is the opt-in pattern for tests that hold a **long-lived** SUT 
(typically a `Microservice` started once in `@BeforeAll`) and want to apply 
per-test overlays without rebuilding it.
+
+The contract:
+
+- The SUT already exists. Its bean store was constructed normally (no Mode 
INJECT pre-installation required).
+- The extension is `attach(...)`-ed to the SUT's bean store at setup time.
+- For each test scope, the extension calls 
`WritableBeanStore.pushOverlay(BeanStore)` to layer the `@TestBean`-built 
overlay onto the live store, then `popOverlay(Snapshot)` to remove it 
afterwards.
+- Subsequent tests see the original beans again — no leakage.
+
+Mode OVERLAY requires `@TestBean(mode = Mode.OVERLAY)` on every annotation 
participating in the push/pop scope. Mixing `Mode.INJECT` and `Mode.OVERLAY` 
annotations in the same scope is rejected with a clear `IllegalStateException`.
+
+### Framework primitive — push/pop on `WritableBeanStore`
+
+The Mode OVERLAY mechanism rests on three additions to the bean-store 
interface:
+
+```java
+public interface WritableBeanStore extends BeanStore, AutoCloseable {
+    // ... existing API ...
+
+    /**
+     * Pushes a new overlay onto this store's overlay stack.  Returns a 
Snapshot
+     * that must be passed to popOverlay() to remove it.  Pop order must be 
LIFO.
+     */
+    Snapshot pushOverlay(BeanStore overlay);
+
+    /**
+     * Removes the overlay identified by the supplied Snapshot.  Throws
+     * IllegalStateException if the Snapshot is not the current top of the 
stack
+     * (LIFO violation), if the stack is empty, or if the Snapshot was produced
+     * by a different store.
+     */
+    void popOverlay(Snapshot snapshot);
+}
+```
+
+`Snapshot` is an opaque value type. It identifies one pushed frame and carries 
the store identity that produced it; tests cannot construct one externally. The 
implementation in `BasicBeanStore` uses a `StackOverlay` composed into the 
existing `overridingParent` slot, so pushed frames win over the store's local 
entries and the regular parent chain (matching Mode INJECT's precedence 
semantics).
+
+Out-of-order pops, pop-when-empty, foreign-snapshot pops, and null pushes all 
throw `IllegalStateException` / `NullPointerException` with descriptive 
messages — these are programming errors, surfaced loudly rather than silently 
degraded.
+
+### Worked example — `Microservice` + Mode OVERLAY
+
+```java
+@ExtendWith(JuneauBeanStoreExtension.class)
+class MyMicroserviceTest {
+
+    static Microservice microservice;
+
+    @RegisterExtension
+    final JuneauBeanStoreExtension ext = new JuneauBeanStoreExtension();
+
+    @BeforeAll
+    static void bootMicroservice() throws Exception {
+        microservice = Microservice.create()
+            .build()
+            .start();
+    }
+
+    @AfterAll
+    static void stopMicroservice() throws Exception {
+        microservice.stop();
+    }
+
+    @BeforeEach
+    void attachExtension() {
+        // Point the extension at the long-lived microservice's bean store.
+        // Per-test overlays will be push'd / pop'd against it automatically.
+        ext.attach(microservice.getBeanStore());
+    }
+
+    @TestBean(mode = Mode.OVERLAY)
+    MyExternalApi mockApi = mock(MyExternalApi.class);
+
+    @Test
+    void each_test_sees_only_its_own_overlay() {
+        assertSame(mockApi, 
microservice.getBeanStore().getBean(MyExternalApi.class).orElseThrow());
+        // After this test, the overlay is popped — the next test starts fresh.
+    }
+}
+```
+
+The `attach(...)` call hands the extension a `WritableBeanStore` reference. 
The extension then drives the push/pop lifecycle automatically:
+
+- `@BeforeAll` → push the class-scope overlay (if any `@TestBean(scope = 
CLASS, mode = Mode.OVERLAY)` declarations are present).
+- `@BeforeEach` → push the method-scope overlay.
+- `@AfterEach` → pop the method-scope overlay.
+- `@AfterAll` → pop the class-scope overlay and `detach()`.
+
+The push/pop sequence is LIFO and balanced — JUnit's callback lifecycle 
already guarantees the nesting; the extension never observes a partial state.
+
+### Mode-OVERLAY safety inventory
+
+Mode OVERLAY's overlay-push wins over local bean-store entries and the regular 
parent chain, just like Mode INJECT's `overridingParent` slot. But it does 
**not** un-pin beans the framework has already memoized at boot — `RestContext` 
per-op memoizers and `Microservice` constructor-final fields are immune.
+
+Concretely:
+
+- **Mode-OVERLAY-safe (per-call resolution):** beans resolved per-request via 
`getBean(...)` from application code, `MicroserviceListener` instances 
broadcast on `start()` / `stop()`, named beans looked up by qualifier.
+- **Mode-INJECT-only (boot-time pinned):** `CallLogger`, `SerializerSet`, 
`ParserSet`, `EncoderSet`, `MarshallingContext`, `SwaggerProvider`, 
`OpenApiProvider`, and the other `RestContext` framework defaults the moment 
they've been read through a per-op memoizer. Swap these via Mode INJECT (fresh 
SUT with `.overridingBeanStore(...)`).
+
+If you push a Mode OVERLAY overlay for a Mode-INJECT-only type, the push 
succeeds but the override is silently ineffective for the framework's 
already-pinned reference. The override remains visible to user code that calls 
`getBean(...)` directly. This is the documented limitation captured in 
TODO-35's open question OQ3 — surface a loud test failure when the swap doesn't 
take effect.
+
+### REST and Mode OVERLAY
+
+`MockRestClient` caches `RestContext` per resource class for performance — by 
design, calling `MockRestClient.builder(MyResource.class).build()` twice 
returns two clients that share the same underlying `RestContext`. Wiring Mode 
OVERLAY push/pop into that shared cache cleanly is a larger lift than the v1 
scope allows; instead, the recommendation is:
+
+- **Use Mode INJECT for REST tests.** Build a fresh `MockRestClient` per test 
with `.overridingBeanStore(store)`. The cache cost is amortized for simple 
resources and is the safer default.
+- **Use Mode OVERLAY for `Microservice` tests.** Booting a microservice is 
genuinely expensive; the push/pop pattern pays for itself when ten tests share 
one running microservice.
+
+This split is the v1 ship; revisit Mode OVERLAY for `MockRestClient` if a 
concrete use case demands it.
+
+## See also
+
+- [REST Server — Mixins and Multi-Mount 
Paths](/docs/topics/RestServerCompositionMixinsAndPaths) — the 
`@Rest(mixins=...)` composition primitive that also benefits from `@TestBean` 
overlays.
+- [REST Server — RFC 7807 / 9457 Problem 
Details](/docs/topics/RestServerProblemDetails)
+- [REST Server — Conditional-GET / ETag 
Helpers](/docs/topics/RestServerConditionalGet)
+- [REST Server — Rate-Limiting and Request-Id 
Propagation](/docs/topics/RestServerRateLimitAndRequestId)
+- Apache Juneau Javadoc:
+  - 
[`JuneauBeanStoreExtension`](/site/apidocs/org/apache/juneau/junit5/JuneauBeanStoreExtension.html)
+  - [`TestBean`](/site/apidocs/org/apache/juneau/junit5/TestBean.html)
+  - 
[`TestBeanStore`](/site/apidocs/org/apache/juneau/junit5/TestBeanStore.html)
+  - 
[`BeanStoreOverridable`](/site/apidocs/org/apache/juneau/commons/inject/BeanStoreOverridable.html)
+  - [`Snapshot`](/site/apidocs/org/apache/juneau/commons/inject/Snapshot.html)
diff --git a/sidebars.ts b/sidebars.ts
index 29d4f55422..bf7dd96d43 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -1496,6 +1496,11 @@ const sidebars: SidebarsConfig = {
                                                        id: 
'topics/10.20c.RestServerRateLimitAndRequestId',
                                                        label: '10.20c. 
Rate-Limiting and Request-Id Propagation',
                                                },
+                                               {
+                                                       type: 'doc',
+                                                       id: 
'topics/10.20d.RestServerTestBeanInjection',
+                                                       label: '10.20d. 
Test-time Bean Injection',
+                                               },
                                                {
                                                        type: 'doc',
                                                        id: 
'topics/10.21.BuiltInParameters',

Reply via email to