What kinds of tests or context features break or aren't supported under AOT-mode testing, and how do you handle them? Cover mock/bean-override behavior and @DisabledInAotMode.
answer
- everything = build-time snapshot vs runtime dynamism
- conditionals/profiles frozen at generation
- bean overrides now in MergedContextConfiguration (mostly work)
- @DisabledInAotMode = skip in AOT, run normally on JVM
- each disable = a hole in native coverage
basics
~20 sAOT freezes the context at build time, so anything decided at runtime — dynamic bean registration, environment-dependent conditionals, context loaders that don't support AOT — can't be represented. Tests that genuinely can't run in AOT are annotated @DisabledInAotMode so they're skipped during AOT generation and AOT-mode runs.
solid answer
~40 sAOT captures a build-time snapshot of the bean factory, so the failure modes are all about runtime dynamism. Contexts using a ContextLoader that isn't AOT-capable can't be generated. @Conditional/profile logic is evaluated during generation and frozen — if it would resolve differently in the real environment, the AOT context diverges. Historically, mock-based bean overriding (@MockBean, now @MockitoBean) was problematic because it mutated the factory at runtime; modern Spring captures override metadata in the MergedContextConfiguration so many overrides are supported, but arbitrary runtime factory manipulation still isn't. The escape hatch is @DisabledInAotMode: annotate the test (or context) and it's skipped during AOT processing and during AOT-mode execution, while still running normally on the JVM without the flag. Architecturally you keep AOT-on-JVM as a gate but accept a small opted-out set.
code
java · 20 linesimport org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.aot.DisabledInAotMode;
// Bean override via @MockitoBean: the override is now part of the
// context's MergedContextConfiguration, so it can participate in AOT.
@SpringBootTest
class PaymentServiceTests {
@MockitoBean PaymentGateway gateway; // captured as override metadata
// ... assertions on stubbed gateway ...
}
// A context that genuinely can't be snapshotted at build time is opted out.
// Skipped during processTestAot generation AND during AOT-mode runs,
// but still runs normally on the plain JVM (flag off).
@DisabledInAotMode("registers beans dynamically from runtime discovery")
@SpringBootTest
class DynamicPluginContextTests {
// ...
}go deeper
Know that some dynamic tests can't run in AOT and there's an annotation to skip them.
Name @DisabledInAotMode and that mocks/conditionals are the usual trouble spots.
Explain the build-time-snapshot root cause and how override metadata now rides in the MCC.
Own the coverage tradeoff: minimize @DisabledInAotMode, reduce conditional divergence, and design the CI gate around the supported subset.
## Root cause of every limitation AOT produces a **build-time snapshot** of the context. Any behavior that depends on **runtime** information the build can't see will either fail to generate or produce a context that differs from the real one. All the specific gotchas are instances of this. ## 1. Non-AOT context loaders The context's `ContextLoader` must support AOT (be a `SmartContextLoader`/`AotContextLoader`). Custom or legacy loaders that only know how to build a context at runtime can't be generated. Such tests must be excluded from AOT. ## 2. Environment-dependent conditionals frozen at build time `@Conditional`, `@Profile`, `@ConditionalOnProperty`, etc. are **evaluated once, during generation**, and the resulting bean set is hard-coded. If the generating environment differs from the target (a property present in prod but not in the build), the AOT context contains the *wrong* beans. This is the most subtle divergence — it doesn't error, it silently differs. Mitigation: generate under representative properties, and validate with AOT-on-JVM. ## 3. Bean overrides and mocks `@MockBean`/`@SpyBean` (and the newer core `@MockitoBean`/`@MockitoSpyBean`, plus generic `@TestBean`) override beans in the factory. Historically these were a hard blocker for AOT/native because the override happened by mutating the bean factory at runtime, which the build-time snapshot couldn't capture. Modern Spring incorporates **bean-override metadata into the `MergedContextConfiguration`**, so the override is part of the context's identity and many mock/override scenarios now work in AOT mode. But: overrides that depend on runtime-computed targets, or that dynamically add beans not known at build time, still won't survive. Treat mocks as 'usually fine now, verify explicitly'. ## 4. Runtime bean registration / BeanFactoryPostProcessor dynamism Programmatic registration that depends on runtime state, or post-processors that add beans based on runtime data, can't be snapshotted faithfully. ## The escape hatch: @DisabledInAotMode `org.springframework.test.context.aot.DisabledInAotMode` (Spring Framework 6.1+) marks a test class (or a context) as not participating in AOT: - During **AOT generation**, that context is **skipped** — so an un-generatable context doesn't fail the whole `processTestAot` build. - During an **AOT-mode run** (`spring.aot.enabled=true`), the test is **skipped**. - Under a **normal JVM run** (flag off), it runs as usual. It optionally takes a reason string for documentation. Use it deliberately for the genuinely-dynamic tests rather than letting them break the build. ## Architectural guidance - **Gate strategy:** run AOT-on-JVM in CI for the supported subset; run the full native test binary less frequently. Track the `@DisabledInAotMode` set as tech debt/known-limitations, not as normal. - **Minimize divergence:** prefer static configuration; make conditionals resolve identically at build and runtime (feed the generator representative config). - **Trust but verify mocks:** even though overrides are largely supported now, add an AOT-on-JVM run over your `@MockitoBean` tests so a regression surfaces cheaply. - **Don't over-disable:** every `@DisabledInAotMode` is a hole in your native coverage; each one should have a rationale. ## Common misconceptions - 'Mocks are impossible in AOT tests' — outdated; metadata-driven overrides work in many cases now. - '@DisabledInAotMode makes the test always skip' — no, it runs normally on the JVM without the AOT flag. - 'If AOT-on-JVM is green, native is guaranteed green' — false; native-only concerns can still bite.
- Does @DisabledInAotMode mean the test never runs?No. It's skipped only during AOT generation and during AOT-mode execution (spring.aot.enabled=true). On a normal JVM run with the flag off, it executes as usual. It just opts the test out of the AOT/native path.
- Are @MockitoBean tests fundamentally incompatible with AOT?Not anymore. Bean-override metadata is folded into the MergedContextConfiguration, so overrides are part of the context identity and many mock scenarios work in AOT mode. Overrides depending on runtime-computed targets or dynamic bean registration still won't survive the build-time snapshot.
- Why can an AOT context contain different beans than a runtime one even with no error?@Conditional/@Profile logic is evaluated once at generation time and frozen. If the generating environment's properties differ from the target's, a different set of beans is baked in — a silent divergence, which is exactly why you validate with AOT-on-JVM under representative config.