You've generated the AOT test artifacts. How do you actually run your tests against the AOT-optimized context on the JVM, and why would you do that instead of building a native image?
answer
- spring.aot.enabled=true is the runtime switch
- generate first (processTestAot), then run
- AotTestContextInitializers consulted by the loader delegate
- fast JVM feedback before slow native build
- @DisabledInAotMode for tests that can't work
basics
~20 sSet the system property spring.aot.enabled=true when running the test task. The TestContext framework then loads the build-time generated context instead of refreshing a fresh one. You do this on the JVM because it's far faster than a native build for catching AOT problems.
solid answer
~40 sTwo separate steps. First, build time: processTestAot generates the AOT artifacts. Second, run time: launch the tests with -Dspring.aot.enabled=true (a Spring property). With that flag set, the TestContext framework's cache-aware context loader checks AotTestContextInitializers, finds the pre-generated ApplicationContextInitializer for the test's context, and loads it directly rather than refreshing a new context via reflection. The tests still run on a normal JVM. You do this because native image builds are slow (minutes) and AOT introduces its own failure modes — missing reflection hints, conditionals resolving differently, unsupported bean overrides. Running AOT-mode tests on the JVM gives a fast feedback loop that surfaces most of those problems before you pay for the native compilation. It's the recommended pre-native validation gate.
code
kotlin · 13 lines// build.gradle.kts — run the existing test suite in AOT mode on the JVM.
tasks.register<Test>("testAot") {
// Reuse the standard test config, but flip the AOT switch.
systemProperty("spring.aot.enabled", "true")
// Requires processTestAot to have run so the generated
// ApplicationContextInitializers exist on the classpath.
dependsOn("processTestAot")
}
// A context that cannot be represented at build time is opted out:
// @DisabledInAotMode
// @SpringBootTest
// class DynamicallyWiredTests { /* ... */ }go deeper
Know the property name spring.aot.enabled=true and that it's the run-time switch.
Explain the generate-then-run split and why JVM AOT is a fast pre-native gate.
Describe how the loader delegate consults AotTestContextInitializers and where JVM AOT still differs from native.
Design the CI split: AOT-on-JVM per PR, full native nightly; opt out unsupported tests deliberately.
## The two-phase model AOT testing splits into **generate** and **run**, and beginners conflate them: 1. **Generate (build time):** `processTestAot` / `process-test-aot` produces the code. This never runs tests. 2. **Run (runtime):** you execute the tests with the **`spring.aot.enabled`** property set to `true`. Only then does the framework *use* the generated artifacts. ## How the flag changes test execution Without the flag, `DefaultCacheAwareContextLoaderDelegate` loads each context by refreshing it normally (reflection-driven). With `spring.aot.enabled=true`, that same delegate consults the generated **`AotTestContextInitializers`** registry. If the current test class is a *supported* AOT test, it uses an `AotContextLoader` path that instantiates the generated `ApplicationContextInitializer` and applies it to a fresh `GenericApplicationContext` — i.e., it loads the **build-time bean factory** directly. No component scanning, no reflective bean-definition parsing. The tests otherwise run exactly as normal JUnit tests **on the JVM**. ## How to set the flag ```kotlin // Gradle tasks.test { systemProperty("spring.aot.enabled", "true") } ``` Or on the command line: `./gradlew test -Dspring.aot.enabled=true` (passed through), or via `SPRING_AOT_ENABLED=true`. In a native test run the flag is effectively always on. ## Why run on the JVM at all? - **Speed:** a GraalVM native image build takes minutes and heavy CPU/RAM. AOT-on-JVM runs in the usual seconds. - **Early detection:** most AOT breakage is *logical*, not native-specific — a context that can't be represented at build time, a bean override that isn't AOT-eligible, a `@Conditional` that evaluated differently at build time. Running AOT-mode on the JVM catches these with a normal debugger and stack traces. - **CI economics:** you can gate every PR with AOT-on-JVM and reserve the full native test run for a nightly/release pipeline. ## Gotchas - If you set the flag but never ran `processTestAot`, there are no generated initializers, so tests either fall back or fail depending on setup — always generate first. - The flag is global for the run; a test that legitimately can't work in AOT should be annotated `@DisabledInAotMode` so it's skipped rather than failing. - AOT-on-JVM is a strong signal but **not** a full substitute for the native run: purely native concerns (a reflection hint that only bites in the closed-world image) may still slip through. ## When to use Use AOT-on-JVM as the routine pre-native gate whenever you ship native images; skip it entirely if you never build native and don't use AOT in production.
- If you forget to run processTestAot but set spring.aot.enabled=true, what happens?There are no generated initializers on the classpath, so the framework has nothing to load in AOT mode for those tests — you'll get failures or a fallback, not a silently-correct run. Generation must precede the AOT run.
- Is passing AOT-on-JVM a guarantee the native test run will pass?No. It catches most logical/build-time-context issues fast, but genuinely native-only concerns (a missing reflection hint that only fails under GraalVM's closed-world assumption) can still surface only in the actual native run.