skip to content

What kinds of failures does running tests as a native image catch that JVM tests miss?

level: middleimportance: should knowfreq 50%

answer

  1. closed-world = everything reachable at build time
  2. reflection / proxies / resources / serialization need hints
  3. JVM tolerates, native binary throws
  4. validates AOT test contexts + RuntimeHints coverage
  5. failure = missing metadata, not logic bug

basics

~20 s

Native tests catch missing reachability metadata: unregistered reflection, JDK proxies, resources, or serialization that the JVM allows at runtime but the closed-world native image forbids. Those show up as native test failures instead of production crashes.

solid answer

~40 s

GraalVM native images run under the closed-world assumption — all reflection, dynamic proxies, resource loading, and serialization must be declared at build time via reachability metadata. On the JVM these happen dynamically and always work, so a JVM-green suite can hide gaps. `nativeTest` compiles and runs the suite as a native binary, so any code path that reflectively loads a class, creates a proxy, reads a classpath resource, or deserializes without a registered hint fails the test rather than the production binary. It also validates that Spring's AOT-processed test contexts start correctly and that your `RuntimeHintsRegistrar` / `@ImportRuntimeHints` declarations actually cover the paths your tests exercise. It does not catch new logical bugs — a failure almost always means missing metadata, and the fix is registering a hint, not changing behavior.

go deeper

for a junior

Know that native images need reflection/resources pre-registered and native tests catch what's missing.

for a middle

Name the closed-world assumption and the categories (reflection, proxies, resources, serialization).

for a senior

Connect failures to RuntimeHints/RuntimeHintsRegistrar fixes and AOT test-context validation.

for a principal

Position native tests as the cheap early gate for metadata gaps before a native release, and scope out Mockito noise.

## The closed-world assumption A GraalVM **native image** is built by static analysis: the builder starts from your entry points and computes everything reachable, then compiles only that into the executable. Anything the analysis cannot see statically is *absent at runtime*. This is the **closed-world assumption**. Dynamic JVM features therefore need explicit **reachability metadata** (also called *hints*) so the builder includes them: - **Reflection** — `Class.forName(...)`, reflective field/method/constructor access. - **JDK dynamic proxies** — `Proxy.newProxyInstance(...)`, common in Spring for `@Configuration` proxying and interface-based proxies. - **Resources** — `getResourceAsStream(...)`, classpath resource bundles, i18n messages. - **Serialization** — Java serialization needs classes registered. On the JVM these all resolve dynamically at runtime, so a JVM test suite never notices missing metadata. In a native image, an unregistered reflective call throws (e.g., a `ClassNotFoundException`, `MissingReflectionRegistrationError`, or a null/absent resource). ## What nativeTest surfaces Because `nativeTest` runs your tests *inside a native binary*, it exercises real code paths under those constraints. It catches: 1. **Missing reflection/resource/proxy hints** for code your tests actually hit — libraries or your own code that reflect without a registered hint. 2. **Broken AOT-processed test contexts** — Spring ahead-of-time generates an `ApplicationContextInitializer` per distinct test context (via `processTestAot`); nativeTest confirms those contexts refresh correctly natively. 3. **Gaps in your own hints** — if you wrote a `RuntimeHintsRegistrar` (registered with `@ImportRuntimeHints` or `RuntimeHintsRegistrar` on the classpath), nativeTest verifies the paths your tests cover are actually registered. ## What it does NOT catch - It is **not** a way to find ordinary logic bugs — those already fail on the JVM. A native-only failure typically means *metadata is missing*, not that behavior differs. - It won't magically make **Mockito** or other bytecode-generating mocking libraries work; those generally are unsupported natively, so such tests may fail for reasons unrelated to your production code. ## Typical fix workflow When a native test fails with a reflection/resource error: 1. Identify the type/resource from the error. 2. Add a hint — a `RuntimeHintsRegistrar` implementation registered via `@ImportRuntimeHints`, or rely on Spring/library-provided hints, or supply GraalVM reachability metadata (`reflect-config.json`, etc.). 3. Re-run `nativeTest`. ## When to lean on it Run native tests before shipping a native binary, especially after adding libraries that use reflection (JSON mappers, ORMs, dynamic clients). It is the cheapest place to discover a missing hint — cheaper than a production native crash.

  • A native test fails with a reflection error on a JSON DTO. What's the fix?
    Register a reflection hint for that type — e.g., a `RuntimeHintsRegistrar` added via `@ImportRuntimeHints` (or `@RegisterReflectionForBinding` for binding types), or rely on the JSON library's Spring-provided hints. The fix is metadata, not a behavior change.
  • Can native tests catch a race condition in your business logic?
    Not specifically — those already reproduce on the JVM. nativeTest targets native-only failure modes: missing reachability metadata and AOT context issues, not general concurrency bugs.

saying these in an interview costs you the question

  • Claiming native tests find logic bugs the JVM suite misses
  • Thinking reflection/resources 'just work' in native images without hints
  • Assuming a native test failure means the application code is wrong (usually it's metadata)

context