skip to content

How do you decide between testing a RuntimeHintsRegistrar in isolation with predicates versus asserting hints on an AOT-processed application context, and how do predicates fit either way?

level: principalimportance: nice to knowfreq 15%

answer

  1. source of RuntimeHints drives the layer, not the assertion
  2. Layer1 isolated registrar / Layer2 AOT context / Layer3 native build
  3. emergent hints → AOT-processed context
  4. predicates same vocabulary everywhere
  5. rejects() to bound surface at scale

basics

~20 s

Use isolated predicate tests for registrars you own — fast contract guards. Use AOT-processed-context hint inspection when hints emerge from bean processing, config properties, or framework integrations. RuntimeHintsPredicates is the assertion tool in both; only the RuntimeHints source differs.

solid answer

~40 s

The decision hinges on *where the hint originates*. If your code contributes hints via an explicit `RuntimeHintsRegistrar`, instantiate it against a fresh `RuntimeHints` and assert with `RuntimeHintsPredicates` — cheapest, most focused, ideal as a per-module contract test. If hints arise from **bean-registration AOT contributions**, `@Reflective` scanning, `@ConfigurationProperties` binding, or third-party AOT processors, an isolated registrar test can't see them; you need to run Spring's AOT processing over a (test) `ApplicationContext` and inspect the *aggregated* `RuntimeHints`. In both cases the assertion vocabulary is identical — `RuntimeHintsPredicates.reflection()/resource()/proxies()/serialization()` with `accepts`/`rejects`. Governance-wise, I'd mandate isolated predicate tests for every owned registrar (cheap regression net), reserve AOT-context assertions for integration-shaped hints, and gate releases with a periodic real native build as ground truth — balancing feedback speed against fidelity.

go deeper

for a junior

Beyond scope; just knows predicate tests exist.

for a middle

Can write isolated tests; the layering decision is advanced.

for a senior

Understands isolated vs AOT-context testing and picks appropriately.

for a principal

Sets org-wide policy across the three layers, mandates precision (rejects), aligns hint ownership with module boundaries, and manages the false-confidence risk.

## The core trade-off: speed vs fidelity All three layers can ultimately use `RuntimeHintsPredicates` for the *assertion*; what changes is **how the `RuntimeHints` under test is produced**, which determines fidelity and cost. ### Layer 1 — Isolated registrar predicate test (cheapest) ```java RuntimeHints hints = new RuntimeHints(); new PaymentModuleHints().registerHints(hints, getClass().getClassLoader()); assertThat(RuntimeHintsPredicates.reflection() .onType(PaymentRequest.class) .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)) .accepts(hints); ``` - **Use when:** you own an explicit `RuntimeHintsRegistrar` (wired via `@ImportRuntimeHints`) or a custom `@Reflective`/`ReflectiveProcessor`. - **Pros:** milliseconds, deterministic, no Spring context, no GraalVM. Perfect as a contributor-facing contract: 'this registrar must always register X'. - **Cons:** sees only that one contributor. Blind to anything produced during real AOT processing. ### Layer 2 — AOT-processed context hint inspection (integration) Spring's AOT engine (`ApplicationContextAotGenerator` / for tests `TestContextAotGenerator`) processes a `BeanFactory` and collects hints from **all** contributors: `BeanRegistrationAotProcessor`s, `BeanFactoryInitializationAotProcessor`s, framework integrations (Spring Data repositories, validation, Jackson/`@ConfigurationProperties` binding), AOP proxy hints, etc. You then assert on the aggregated `RuntimeHints` with the same predicates. - **Use when:** the hints you care about are *emergent* — nobody registers them explicitly; they fall out of how beans are wired. Example: a `@ConfigurationProperties` record needs constructor-binding reflection hints contributed by Spring Boot's processor, not by any registrar you wrote. - **Pros:** far higher fidelity; catches contributor interactions. - **Cons:** slower, heavier setup, still not a real native compile. ### Layer 3 — Actual native build (authoritative, slow) Ground truth. Run in CI on a cadence (nightly/pre-release) because it costs minutes. Predicates don't apply here; the binary either runs or fails. ## Principal-level governance - **Default policy:** every module that ships an owned `RuntimeHintsRegistrar` must carry Layer-1 predicate tests — they're a near-free regression net and double as executable documentation of the registrar's contract. - **Escalate to Layer 2** only where hints are emergent (config-props binding, repository proxies, serialization across boundaries) — don't pay integration cost for hints a Layer-1 test already pins. - **Layer 3 as a gate**, not a per-commit check: it's the only true completeness signal but too slow for the inner loop. - **Negative assertions matter at scale:** use `.rejects(...)` to bound reflection/proxy/serialization surface — over-registration bloats the image and widens attack surface. A principal reviews for *precision*, not just presence. - **Beware false confidence:** a green Layer-1 suite says 'my registrars behave', never 'the app is native-ready'. Communicate that distinction so teams don't skip Layers 2–3. ## Kotlin / Spring Modulith angle In a modular codebase, scoping a predicate test per module registrar aligns hint ownership with module boundaries — each module proves its own native contract, mirroring how module `*Api` surfaces are tested. This keeps the fast feedback local and makes missing-hint regressions attributable to the owning module.

  • Give an example of an 'emergent' hint an isolated registrar test would miss.
    Constructor-binding reflection hints for a @ConfigurationProperties record — contributed by Spring Boot's AOT processing of the bean, not by any RuntimeHintsRegistrar you wrote. You'd only see it by inspecting an AOT-processed context.
  • Why still keep isolated predicate tests if you also run native builds in CI?
    Feedback speed and attribution. Native builds take minutes and only say pass/fail; a per-registrar predicate test runs in milliseconds, pins an exact contract, and points precisely at the owning module when it regresses.

context