skip to content

AOT-Mode Tests

Test AOT processing generates the test contexts ahead of time so tests exercise the same build-time bean factory while still running on the JVM. This is the cheap way to catch AOT breakage before a native build.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

4

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?

level: middleimportance: must knowfreq 45%

answer

  1. spring.aot.enabled=true is the runtime switch
  2. generate first (processTestAot), then run
  3. AotTestContextInitializers consulted by the loader delegate
  4. fast JVM feedback before slow native build
  5. @DisabledInAotMode for tests that can't work

basics

~20 s

Set 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 s

Two 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
kotlin
// 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

for a junior

Know the property name spring.aot.enabled=true and that it's the run-time switch.

for a middle

Explain the generate-then-run split and why JVM AOT is a fast pre-native gate.

for a senior

Describe how the loader delegate consults AotTestContextInitializers and where JVM AOT still differs from native.

for a principal

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.

context

open as a page

What are AOT-mode tests in Spring, and what does the process-test-aot / processTestAot step actually do?

level: juniorimportance: should knowfreq 35%

basics

~20 s

AOT (ahead-of-time) test processing pre-builds each test's Spring ApplicationContext at build time instead of at runtime. The processTestAot task (Gradle) or process-test-aot goal (Maven) generates that code so tests can run against a pre-optimized context.

open as a page

Walk through what TestContextAotGenerator does internally when it processes your test classes. What does it emit and why is context de-duplication central?

level: seniorimportance: should knowfreq 30%

basics

~20 s

TestContextAotGenerator scans test classes, resolves each one's MergedContextConfiguration, groups tests that share an identical context, and generates one ApplicationContextInitializer per unique context plus native runtime hints and registry classes (AotTestContextInitializers, AotTestAttributes) that map tests to their context.

open as a page

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.

level: principalimportance: should knowfreq 22%

basics

~20 s

AOT 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.

open as a page