skip to content

RuntimeHintsPredicates

RuntimeHintsPredicates lets a plain unit test assert that a registrar declared the hints you expect, long before the native build runs. A neat answer to 'how do you test something that only fails at image build time'.

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

explore

questions

5

Write a test that asserts a registrar registered reflection access to a type's declared constructors.

level: middleimportance: must knowfreq 45%

answer

  1. new RuntimeHints() → registerHints → assertThat.accepts
  2. reflection().onType(X).withMemberCategory(...)
  3. MemberCategory.INVOKE_DECLARED_CONSTRUCTORS
  4. subset match: contains-at-least
  5. rejects() for over-registration

basics

~10 s

Run the registrar against a fresh RuntimeHints, then assert RuntimeHintsPredicates.reflection().onType(MyType.class).withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS) accepts those hints using AssertJ's .accepts(hints).

solid answer

~30 s

You create a `RuntimeHints` instance, invoke your `RuntimeHintsRegistrar.registerHints(hints, classLoader)`, then assert with a reflection predicate refined by member category. `RuntimeHintsPredicates.reflection().onType(X.class)` checks a reflection hint exists on the type; chaining `.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)` narrows it to require that the type's declared constructors are invokable. Wrap it in AssertJ: `assertThat(predicate).accepts(hints)` (or `.rejects(hints)` for the negative case). The member-category enum values (e.g. `INVOKE_DECLARED_CONSTRUCTORS`, `INVOKE_PUBLIC_METHODS`, `DECLARED_FIELDS`) mirror what you'd have passed to `hints.reflection().registerType(...)`. The predicate matches as long as the requested category is present in the registered hint — extra categories on the hint don't cause a rejection.

code

java · 27 lines
java
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import static org.assertj.core.api.Assertions.assertThat;

class MyHints implements RuntimeHintsRegistrar {
    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
        hints.reflection().registerType(MyDto.class,
                MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
    }
}

class MyHintsTests {
    @Test
    void contributesConstructorHint() {
        RuntimeHints hints = new RuntimeHints();
        new MyHints().registerHints(hints, getClass().getClassLoader());

        assertThat(RuntimeHintsPredicates.reflection()
                .onType(MyDto.class)
                .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS))
            .accepts(hints);
    }
}

go deeper

for a junior

Recognize the three-step shape and that member categories exist.

for a middle

Can write the full test and pick the correct MemberCategory; knows accepts vs rejects.

for a senior

Understands contains-at-least matching and the over-broad onType pitfall; uses rejects for negative assertions.

for a principal

Would codify this as a convention for library hint tests and review member-category precision.

## The standard test shape Every hint predicate test follows the same three steps: **(1)** build an empty `RuntimeHints`, **(2)** drive your contribution into it, **(3)** assert with a predicate. ```java import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import static org.assertj.core.api.Assertions.assertThat; class MyHintsTests { @Test void registersConstructorReflectionForDto() { RuntimeHints hints = new RuntimeHints(); new MyHints().registerHints(hints, getClass().getClassLoader()); assertThat(RuntimeHintsPredicates.reflection() .onType(MyDto.class) .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)) .accepts(hints); } } ``` ## Anatomy of the reflection predicate - `RuntimeHintsPredicates.reflection()` returns a `ReflectionHintsPredicates` builder. - `.onType(Class)` (or `.onType(TypeReference)`) → a predicate that's true when *any* reflection hint exists for that type. - `.onConstructor(Constructor)`, `.onMethod(Class, name)`, `.onField(Class, name)` → narrow to a specific member. - `.withMemberCategory(MemberCategory)` → require a single category is present on the type hint. - `.withMemberCategories(MemberCategory...)` → require **all** listed categories. - `.withAnyMemberCategory(MemberCategory...)` → require **at least one** of the listed categories. Common `MemberCategory` values: `INVOKE_DECLARED_CONSTRUCTORS`, `INVOKE_PUBLIC_CONSTRUCTORS`, `INVOKE_DECLARED_METHODS`, `INVOKE_PUBLIC_METHODS`, `DECLARED_FIELDS`, `PUBLIC_FIELDS`. ## Matching semantics — the key gotcha The predicate is a **'contains at least'** check. `.withMemberCategory(INVOKE_DECLARED_CONSTRUCTORS)` accepts a hint that was registered with `INVOKE_DECLARED_CONSTRUCTORS` *and* `INVOKE_DECLARED_METHODS`. So a predicate that asserts a subset will pass even if the registrar was more generous. Conversely, `.onType(X)` **without** a member category accepts as soon as the type is registered *at all* (even just for introspection) — so an over-broad assertion can hide the fact that a needed category is missing. Assert the specific category you actually depend on. ## accepts vs rejects `assertThat(predicate).accepts(hints)` asserts the predicate returns `true`; `.rejects(hints)` asserts `false`. Use `.rejects` to prove you did **not** over-register (e.g. that an internal type was *not* exposed to reflection). ## Kotlin note In Kotlin the same API applies; use `MyDto::class.java` for the `Class` argument. The AssertJ assertion and predicate chaining are identical.

  • Difference between withMemberCategory, withMemberCategories, and withAnyMemberCategory?
    withMemberCategory requires one specific category present; withMemberCategories requires ALL listed categories present; withAnyMemberCategory requires AT LEAST ONE of the listed categories.
  • Why might onType(X) with no member category be a weak assertion?
    It passes as soon as the type is registered for any reason, including bare introspection, so it won't catch a missing INVOKE category your code actually needs. Assert the specific category.

context

open as a page

What is RuntimeHintsPredicates and why would you use it in a unit test?

level: juniorimportance: should knowfreq 35%

basics

~20 s

RuntimeHintsPredicates is a Spring test helper that builds predicates checking whether a RuntimeHintsRegistrar registered the expected reflection, resource, or proxy hints — so you can assert hints in a fast unit test instead of running a slow native build.

open as a page

Beyond reflection, what other hint categories can RuntimeHintsPredicates assert, and how?

level: middleimportance: should knowfreq 30%

basics

~10 s

It also covers resources, JDK proxies, and serialization: resource().forResource("...") and resource().forBundle("messages"), proxies().forInterfaces(A.class, B.class), and serialization().onType(X.class) — each returns a Predicate<RuntimeHints> you assert with AssertJ.

open as a page

Where do the hints being tested come from, and what are the limits of predicate-based hint tests in a native-readiness strategy?

level: seniorimportance: should knowfreq 25%

basics

~20 s

Hints come from RuntimeHintsRegistrar (via @ImportRuntimeHints), @Reflective-driven processors, and AOT bean-registration contributions. Predicate tests prove a specific hint was registered but not that the app's whole hint set is complete — you still need an actual native build or AOT test.

open as a page

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%

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.

open as a page