skip to content

How do you use ApplicationContextRunner to verify a @ConditionalOnProperty-guarded bean appears only when the property is set? Show the assertions.

level: middleimportance: must knowfreq 38%

answer

  1. withPropertyValues == application.properties into Environment
  2. hasSingleBean vs doesNotHaveBean = on vs backed-off
  3. test both present and absent cases
  4. matchIfMissing → run with no property
  5. use the returned runner (immutable)

basics

~10 s

Chain withPropertyValues("feature.enabled=true") before run(), then inside the lambda assert assertThat(context).hasSingleBean(Foo.class). For the off case, omit or set the property false and assert doesNotHaveBean(Foo.class).

solid answer

~40 s

Register the auto-config with withConfiguration(AutoConfigurations.of(...)), then drive the property via withPropertyValues, which injects entries into the context's Environment exactly as if they came from application.properties. Run twice: once with the property enabling the bean and once without. Inside each run() lambda use AssertJ against the AssertableApplicationContext — assertThat(context).hasSingleBean(MyService.class) for the enabled case and doesNotHaveBean(MyService.class) for the disabled/absent case. Because the runner is immutable, each withPropertyValues(...) returns a new configured runner, so a shared base runner field stays untouched between the two cases. This is the canonical pattern for testing @ConditionalOnProperty, and the same shape (withPropertyValues + hasSingleBean/doesNotHaveBean) covers matchIfMissing behaviour: to prove matchIfMissing=true you run with no property at all and still expect the bean.

code

java · 12 lines
java
@Test
void respectsConditionalOnProperty() {
    ApplicationContextRunner runner = new ApplicationContextRunner()
            .withConfiguration(AutoConfigurations.of(MyAutoConfiguration.class));

    // enabled -> bean present
    runner.withPropertyValues("my.feature.enabled=true")
          .run(ctx -> assertThat(ctx).hasSingleBean(MyService.class));

    // absent -> bean backs off (no matchIfMissing)
    runner.run(ctx -> assertThat(ctx).doesNotHaveBean(MyService.class));
}

go deeper

for a junior

Can add a property and assert the bean is or isn't there.

for a middle

Tests both branches and knows the immutability pitfall of the fluent builder.

for a senior

Also covers matchIfMissing and chains getBean(...) to assert configured state.

for a principal

Treats condition coverage (all branches incl. matchIfMissing) as a completeness bar for a starter's test suite.

## The scenario Auto-configuration commonly gates a bean like this: ```java @AutoConfiguration public class MyAutoConfiguration { @Bean @ConditionalOnProperty(prefix = "my.feature", name = "enabled", havingValue = "true") MyService myService() { return new MyService(); } } ``` You want tests proving: (1) property `true` → bean present, (2) property `false`/absent → bean absent. ## withPropertyValues `withPropertyValues(String... pairs)` adds `key=value` entries into the context's `Environment` as a property source, identical in effect to putting them in `application.properties`. It accepts multiple pairs and can be called repeatedly (values accumulate). This is how `@ConditionalOnProperty` sees them, because that condition resolves against the `Environment`. ## The two-case test ```java private final ApplicationContextRunner runner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(MyAutoConfiguration.class)); @Test void beanPresentWhenEnabled() { runner.withPropertyValues("my.feature.enabled=true") .run(ctx -> assertThat(ctx).hasSingleBean(MyService.class)); } @Test void beanAbsentWhenDisabled() { runner.withPropertyValues("my.feature.enabled=false") .run(ctx -> assertThat(ctx).doesNotHaveBean(MyService.class)); } @Test void beanAbsentWhenMissing() { runner.run(ctx -> assertThat(ctx).doesNotHaveBean(MyService.class)); } ``` ## Key assertions - `hasSingleBean(Type.class)` — exactly one bean of that type. Preferred over `hasBean(name)` because it's type-based and catches accidental duplicates. - `doesNotHaveBean(Type.class)` — no bean of that type; this is what proves a condition *backed off*. - `getBean(Type.class)` — chain further AssertJ on the actual instance, e.g. `assertThat(ctx).getBean(MyService.class).extracting("timeout").isEqualTo(30)`. ## matchIfMissing gotcha If the condition uses `matchIfMissing = true`, the bean should exist even with **no** property. Test that by running with no `withPropertyValues` and asserting `hasSingleBean`. Conversely a common bug is forgetting `matchIfMissing` and being surprised the bean is absent by default — the runner makes that explicit. ## Immutability matters here Each `withPropertyValues(...)` returns a *new* runner. If you wrote `runner.withPropertyValues(...);` on its own line and then called `runner.run(...)`, the property would be lost — you must use the returned instance (chain it, as above). This is the single most common mistake. ## Property value types Everything you pass is a string, parsed by Spring's normal relaxed binding / `@ConditionalOnProperty` `havingValue` string comparison. `havingValue="true"` matches the string `true`; there's no type coercion surprise because the condition compares strings.

  • You wrote `runner.withPropertyValues("x=1"); runner.run(...);` on two lines and the property isn't applied. Why?
    The runner is immutable — withPropertyValues returns a new instance and leaves the original unchanged. You discarded the configured instance. Chain the calls or reassign: runner = runner.withPropertyValues(...).
  • How would you test that the bean is present by default via matchIfMissing=true?
    Run the context with no withPropertyValues at all and assert hasSingleBean. That proves the condition matches when the property is absent.

saying these in an interview costs you the question

  • Calling withPropertyValues on its own line then run() on the original runner (property lost)
  • Only testing the positive case and never asserting doesNotHaveBean for back-off
  • Using hasBean(name) with a guessed name instead of type-based hasSingleBean
  • Thinking withPropertyValues sets JVM system properties (that's withSystemProperties)

context