skip to content

How do you use ApplicationContextRunner with FilteredClassLoader to test @ConditionalOnClass / @ConditionalOnMissingClass back-off?

level: seniorimportance: should knowfreq 24%

answer

  1. FilteredClassLoader hides a class -> ClassNotFoundException
  2. withClassLoader(new FilteredClassLoader(SomeLib.class))
  3. hide class = test @ConditionalOnClass back-off
  4. inverse: @ConditionalOnMissingClass fallback appears
  5. String ctor hides by package prefix; resource filter for @ConditionalOnResource

basics

~10 s

Use withClassLoader(new FilteredClassLoader(SomeLib.class)) to hide a class as if it weren't on the classpath, then run() and assert the conditionally-created bean is absent — proving @ConditionalOnClass backed off.

solid answer

~40 s

@ConditionalOnClass gates a bean on a type being present on the classpath, so to test the 'library missing' branch you need a way to make a class appear absent without actually changing your test dependencies. FilteredClassLoader (org.springframework.boot.test.util) wraps the normal classloader and throws ClassNotFoundException for the classes (or packages) you pass it. Feed it via withClassLoader(new FilteredClassLoader(SomeLib.class)); the runner builds the context under that classloader, so @ConditionalOnClass evaluates as if the library is gone and the bean backs off — assert doesNotHaveBean(...). The complement is @ConditionalOnMissingClass, where hiding the class should now *create* the fallback bean, so you assert hasSingleBean(...). Without FilteredClassLoader you could only test the 'present' branch, since the class is really on your test classpath. It accepts Class objects or String package/class-name prefixes.

code

java · 14 lines
java
import org.springframework.boot.test.util.FilteredClassLoader;

@Test
void gracefullyBacksOffWhenOptionalLibMissing() {
    ApplicationContextRunner runner = new ApplicationContextRunner()
            .withConfiguration(AutoConfigurations.of(MyAutoConfiguration.class));

    // present -> bean created
    runner.run(ctx -> assertThat(ctx).hasSingleBean(MyService.class));

    // hide SomeLib -> @ConditionalOnClass(SomeLib.class) backs off
    runner.withClassLoader(new FilteredClassLoader(SomeLib.class))
          .run(ctx -> assertThat(ctx).doesNotHaveBean(MyService.class));
}

go deeper

for a junior

Likely unaware; may not know you can simulate a missing class.

for a middle

Knows withClassLoader + FilteredClassLoader exists to hide a class.

for a senior

Correctly tests both @ConditionalOnClass and @ConditionalOnMissingClass branches and filters the right type.

for a principal

Sees classpath-permutation coverage as a robustness contract for a starter and knows the resource-filter variant too.

## Why you need it `@ConditionalOnClass(SomeLib.class)` means 'create this bean only if `SomeLib` is on the classpath'. `@ConditionalOnMissingClass("com.example.SomeLib")` is the inverse. In a test module, the library is usually a real dependency, so the class *is* present — you can easily test the present branch but not the absent branch. You can't remove a Maven/Gradle dependency per test. ## FilteredClassLoader `org.springframework.boot.test.util.FilteredClassLoader` is a `ClassLoader` that delegates normally except for a set of hidden targets, for which it throws `ClassNotFoundException`. You construct it with either: - `new FilteredClassLoader(SomeLib.class, Other.class)` — hide specific classes. - `new FilteredClassLoader("com.example.somelib")` — hide by package/prefix (String). - `new FilteredClassLoader(FilteredClassLoader.ClassPathResourceFilter...)` / resource filters — also hide *resources* (useful for testing `@ConditionalOnResource`). ## Wiring it into the runner `withClassLoader(ClassLoader)` tells the runner to build the context under that classloader. All condition evaluation (which uses classpath presence checks) then happens against the filtered view: ```java private final ApplicationContextRunner runner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(MyAutoConfiguration.class)); @Test void createsBeanWhenLibraryPresent() { runner.run(ctx -> assertThat(ctx).hasSingleBean(MyService.class)); } @Test void backsOffWhenLibraryAbsent() { runner.withClassLoader(new FilteredClassLoader(SomeLib.class)) .run(ctx -> assertThat(ctx).doesNotHaveBean(MyService.class)); } ``` ## @ConditionalOnMissingClass — the inverse If your auto-config provides a *fallback* bean when a class is absent: ```java @Bean @ConditionalOnMissingClass("com.example.SomeLib") FallbackService fallback() { ... } ``` Hiding the class should now make the fallback appear: ```java runner.withClassLoader(new FilteredClassLoader(SomeLib.class)) .run(ctx -> assertThat(ctx).hasSingleBean(FallbackService.class)); ``` ## Gotchas - **Hide the class the condition names, not your own bean.** `@ConditionalOnClass` checks the *third-party* type; filter that type, not `MyService`. - **Package-prefix filtering is broad.** `new FilteredClassLoader("com.example.somelib")` hides everything under that prefix — handy but make sure you don't accidentally hide test infrastructure. - **Resources vs classes.** For `@ConditionalOnResource` use the resource-filter constructor, not the class one. - **It only affects the context built by that run.** Because the runner is immutable and per-run isolated, the filtered classloader doesn't leak to other runs or tests. - **Metadata-based back-off.** Spring Boot's `@ConditionalOnClass` actually reads class names as *strings* from bytecode metadata to avoid loading, but `FilteredClassLoader` still correctly simulates absence because the classpath resource for the class is hidden. ## When to reach for it Primarily library/starter authors verifying their auto-configuration degrades gracefully when an optional dependency is missing — a core robustness guarantee of a well-behaved starter.

  • Your @ConditionalOnClass names a third-party type but your test hides your own MyService class. Why does the test fail to prove anything?
    The condition checks the third-party type's presence, not MyService. Hiding MyService doesn't change the condition's outcome (and may even break context creation). You must filter the exact type the condition references.
  • How would you simulate a missing resource for @ConditionalOnResource rather than a missing class?
    Use FilteredClassLoader's resource-filtering constructor (a ClassPathResourceFilter) so getResource for that path returns null, rather than the class-hiding constructor.

saying these in an interview costs you the question

  • Filtering your own bean class instead of the third-party class the condition names
  • Believing you must remove the Gradle/Maven dependency to test the absent branch
  • Thinking FilteredClassLoader leaks to other tests (each run is isolated)
  • Confusing @ConditionalOnClass (needs class present) with @ConditionalOnMissingClass (needs it absent)

context