Class JuneauBeanStoreExtension

java.lang.Object
org.apache.juneau.bct.junit.JuneauBeanStoreExtension
All Implemented Interfaces:
org.junit.jupiter.api.extension.AfterAllCallback, org.junit.jupiter.api.extension.AfterEachCallback, org.junit.jupiter.api.extension.BeforeAllCallback, org.junit.jupiter.api.extension.BeforeEachCallback, org.junit.jupiter.api.extension.Extension, org.junit.jupiter.api.extension.ParameterResolver, org.junit.jupiter.api.extension.TestInstantiationAwareExtension

public class JuneauBeanStoreExtension extends Object implements org.junit.jupiter.api.extension.BeforeAllCallback, org.junit.jupiter.api.extension.AfterAllCallback, org.junit.jupiter.api.extension.BeforeEachCallback, org.junit.jupiter.api.extension.AfterEachCallback, org.junit.jupiter.api.extension.ParameterResolver
JUnit 5 extension that discovers @TestBean-annotated members on a test class and exposes them as a TestBeanStore overlay.
Mode INJECT — fresh-instance with overrides (default):

The extension supports two wiring patterns. Mode INJECT is the default and what every @TestBean-based test ships with out of the box. The contract:

  • The SUT (a RestContext via MockRestClient, a Microservice, a SerializerSet, etc.) is constructed after the overlay is built. The overlay is threaded into the builder via overridingBeanStore(...) (see BeanStoreOverridable), 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 resolution chain, every framework-managed bean lookup consults it before any local @Bean factory or memoizer-backed default. Mode INJECT is therefore universal: all bean types are eligible for replacement, including those that subsequently get pinned into a per-op memoizer.
  • The overlay's lifetime is bounded by the SUT's lifetime. For a per-test fresh SUT (the most common shape), the overlay is discarded when the SUT goes out of scope at @AfterEach. For a per-class shared SUT (built once in @BeforeAll), the overlay survives until the SUT is torn down at @AfterAll. No explicit push/pop is required.

@TestBean, TestBeanStore, BeanStoreOverridable, and the overridingBeanStore(...) setters on MockRestClient.Builder, Microservice.Builder, SerializerSet.Builder, ParserSet.Builder, and EncoderSet.Builder all implement this Mode INJECT contract.

Mode OVERLAY — existing-instance with push/pop:

Mode OVERLAY is the opt-in pattern for tests that hold a long-lived SUT (typically a Microservice booted in @BeforeAll) and want to apply per-test overlays without rebuilding it. The test calls attach(WritableBeanStore) to point the extension at the SUT's bean store; the extension then uses WritableBeanStore.pushOverlay(BeanStore) at beforeEach / beforeAll and WritableBeanStore.popOverlay(Snapshot) at afterEach / afterAll so the SUT's bean store is restored between tests.

Mode OVERLAY requires @TestBean(mode = Mode.OVERLAY) on the participating annotations. Mixing Mode.INJECT and Mode.OVERLAY on the same scope is rejected with a clear IllegalStateException.

Overlay storage:

The extension owns one or two TestBeanStore overlays per test execution:

  • A class-scope overlay built at beforeAll, containing every @TestBean(scope = CLASS) declared on static fields/methods of the test class hierarchy. Lives until afterAll.
  • A method-scope overlay built at beforeEach, containing every @TestBean(scope = METHOD) declared on instance (or static, for convenience) fields/methods. The method-scope overlay's parent is set to the class-scope overlay (if any), so a method-scope override of (Type, name) shadows a class-scope override of the same pair while still letting the class-scope's other entries fall through. Lives until afterEach.

Wiring into the system under test is explicit — the extension never reflects on the test class to find a RestContext / Microservice field and never mutates the SUT after construction. Test authors retrieve the current overlay via getStore() and plug it into the SUT's builder hook:

@ExtendWith(JuneauBeanStoreExtension.class) class MyResourceTest { @TestBean MyService mockSvc = Mockito.mock(MyService.class); @Test void aTest(TestBeanStore store) { var client = MockRestClient.create(MyResource.class) .overridingBeanStore(store) .build(); client.get("/svc").run().assertStatus().is(200); } }

Two registration styles are supported:

  • Declarative: @ExtendWith(JuneauBeanStoreExtension.class) on the test class.
  • Programmatic: a @RegisterExtension field initialized with create(), so the test can invoke getStore() from within @BeforeAll / @BeforeEach bodies as well as from test methods.
See Also:
  • TestBean - The annotation discovered by this extension.
  • TestBeanStore - The overlay type the extension produces.
  • Scope - Per-method vs per-class lifecycle selector.
  • Mode - Mode INJECT (fresh instance) vs Mode OVERLAY (existing instance push/pop) selector.
Since:
10.0.0
  • Constructor Details

  • Method Details

    • create

      Creates a new, unconfigured extension instance for programmatic registration via @RegisterExtension.

      Equivalent to new JuneauBeanStoreExtension() but reads more naturally at the registration site:

      @RegisterExtension JuneauBeanStoreExtension ext = JuneauBeanStoreExtension.create();

      Returns:
      A new extension instance. Never null.
      Since:
      10.0.0
    • attach

      Points this extension at a long-lived SUT's WritableBeanStore for Mode OVERLAY push/pop wiring.

      Call from a @BeforeAll or @BeforeEach body when the participating @TestBean declarations use Mode.OVERLAY. Once attached, the extension will:

      • push the class-scope overlay onto the attached store at beforeAll (if a non-empty class-scope overlay exists), and pop it at afterAll.
      • Push the method-scope overlay at beforeEach and pop it at afterEach.

      Calling attach(...) a second time before detach() is permitted — the most recent attachment wins. If never called, the extension stays in Mode INJECT.

      Parameters:
      store - The bean store to push/pop overlays against. Must not be null.
      Returns:
      This extension, for fluent chaining.
      Throws:
      NullPointerException - If store is null.
      Since:
      10.0.0
    • detach

      Clears the attached bean store reference.

      Called automatically at afterAll; tests may also call it explicitly between scenarios. Has no effect if the extension is not currently attached.

      Returns:
      This extension, for fluent chaining.
      Since:
      10.0.0
    • getAttachedStore

      Returns the attached bean store, or empty if none.

      Surface for diagnostic / testability purposes — the push/pop lifecycle uses the field directly.

      Returns:
      The attached store, or Optional.empty() if not currently attached.
      Since:
      10.0.0
    • getStore

      Returns the current effective overlay — method-scope if a test method is executing, otherwise the class-scope overlay.

      This is the accessor most tests plug into builder hooks such as MockRestClient.Builder.overridingBeanStore(...).

      Returns:
      The current overlay. Never null.
      Throws:
      IllegalStateException - If no class-scope or method-scope overlay is currently active (e.g. the accessor is called from a @BeforeAll body and the extension is not yet bound to a test class).
      Since:
      10.0.0
    • getStore

      Returns the overlay for the specified scope, if one is currently active.

      Allows a test to reach past the current effective overlay and inspect a specific lifecycle layer. For example, a test running under both a class-scope and a method-scope overlay can use getStore(Scope.CLASS) to retrieve the shared per-class layer directly.

      Parameters:
      scope - The lifecycle scope to query. Must not be null.
      Returns:
      The overlay for that scope, or Optional.empty() if no such overlay is active.
      Since:
      10.0.0
    • beforeAll

      public void beforeAll(org.junit.jupiter.api.extension.ExtensionContext context)
      Specified by:
      beforeAll in interface org.junit.jupiter.api.extension.BeforeAllCallback
    • afterAll

      public void afterAll(org.junit.jupiter.api.extension.ExtensionContext context)
      Specified by:
      afterAll in interface org.junit.jupiter.api.extension.AfterAllCallback
    • beforeEach

      public void beforeEach(org.junit.jupiter.api.extension.ExtensionContext context)
      Specified by:
      beforeEach in interface org.junit.jupiter.api.extension.BeforeEachCallback
    • afterEach

      public void afterEach(org.junit.jupiter.api.extension.ExtensionContext context)
      Specified by:
      afterEach in interface org.junit.jupiter.api.extension.AfterEachCallback
    • supportsParameter

      public boolean supportsParameter(org.junit.jupiter.api.extension.ParameterContext parameterContext, org.junit.jupiter.api.extension.ExtensionContext extensionContext)
      Specified by:
      supportsParameter in interface org.junit.jupiter.api.extension.ParameterResolver
    • resolveParameter

      public Object resolveParameter(org.junit.jupiter.api.extension.ParameterContext parameterContext, org.junit.jupiter.api.extension.ExtensionContext extensionContext)
      Specified by:
      resolveParameter in interface org.junit.jupiter.api.extension.ParameterResolver
    • discoverOverrides

      Discovers the @TestBean overrides declared on a test instance (and its class hierarchy) and returns them as a single JuneauBeanStoreExtension.OverrideSet — the public composition entry point for other JUnit 5 extensions (e.g. @MicroserviceTest) that need the @TestBean substrate without driving this extension's own lifecycle.

      Both @TestBean(scope = CLASS) (static) and @TestBean(scope = METHOD) (instance) declarations are collected into one flattened overlay (method-scope chained over class-scope), so a composing extension that boots a single system-under-test per scope sees every declared override. The returned JuneauBeanStoreExtension.OverrideSet.mode() is the unified mode of all declarations (defaulting to Mode.INJECT when none are present); mixing INJECT and OVERLAY in one scope throws IllegalStateException.

      Parameters:
      testInstance - The test-class instance to scan. Must not be null.
      Returns:
      The discovered overrides + their unified mode. Never null; JuneauBeanStoreExtension.OverrideSet.isEmpty() is true when no @TestBean members were found.
      Since:
      10.0.0