skip to content

How do you assert on a context that fails to start with ApplicationContextRunner, and when should you prefer the runner over @SpringBootTest?

level: seniorimportance: should knowfreq 20%

answer

  1. failure captured, not thrown
  2. hasFailed() / hasNotFailed()
  3. getFailure() -> AssertJ ThrowableAssert chain
  4. hasRootCauseInstanceOf + hasMessageContaining
  5. runner for conditions/perms; @SpringBootTest for full app + server

basics

~10 s

Inside run(), the failure is captured not thrown. Use assertThat(context).hasFailed() and assertThat(context).getFailure() to inspect the startup exception (e.g. rootCause / message). Prefer the runner over @SpringBootTest for fast, isolated auto-config/condition tests.

solid answer

~40 s

When a context fails to start, ApplicationContextRunner does not rethrow the exception out of run(); it captures it and hands you a failed AssertableApplicationContext. You assert with assertThat(context).hasFailed() to confirm failure, then assertThat(context).getFailure() to get the Throwable and chain AssertJ — e.g. .getFailure().hasRootCauseInstanceOf(BeanCreationException.class).hasMessageContaining("required property"). You can also assert the *absence* of failure with .hasNotFailed(). This makes negative tests (bad config must fail with a clear message) first-class. Prefer the runner over @SpringBootTest when you're testing auto-configuration and @Conditional outcomes across many property/classpath permutations: it's dramatically faster (no component scan, no server, isolated per run) and expresses failure/back-off assertions cleanly. Prefer @SpringBootTest (or slices like @WebMvcTest) when you need the real, fully wired application, a servlet container, or end-to-end behaviour — the runner intentionally doesn't give you those.

code

java · 19 lines
java
@Test
void contextFailsWithClearMessageOnBadConfig() {
    new ApplicationContextRunner()
        .withConfiguration(AutoConfigurations.of(MyAutoConfiguration.class))
        .withPropertyValues("my.timeout=-1")   // invalid, @Validated rejects
        .run(ctx -> assertThat(ctx)
            .hasFailed()
            .getFailure()
            .hasMessageContaining("timeout"));
}

@Test
void contextStartsCleanlyWithValidConfig() {
    new ApplicationContextRunner()
        .withConfiguration(AutoConfigurations.of(MyAutoConfiguration.class))
        .withPropertyValues("my.timeout=30")
        .run(ctx -> assertThat(ctx).hasNotFailed()
                                   .hasSingleBean(MyService.class));
}

go deeper

for a junior

May not know failures are captured; likely expects an exception to be thrown.

for a middle

Uses hasFailed()/getFailure() for negative tests and knows the runner is faster than @SpringBootTest.

for a senior

Chains ThrowableAssert on getFailure() and articulates the runner-vs-@SpringBootTest trade-off precisely.

for a principal

Designs a layered test strategy: runner for the conditional matrix, a minimal set of @SpringBootTest/slice tests for real assembly and I/O.

## Failures are captured, not thrown A normal Spring context throws on startup failure. `ApplicationContextRunner` instead **captures** any startup exception and gives your `run()` lambda a *failed* `AssertableApplicationContext`. This is deliberate: it lets you write positive-path and failure-path tests with the same fluent style, and it means a failing context won't blow up your test before you can assert on *why* it failed. ## Failure assertions - `assertThat(context).hasFailed()` — asserts the context failed to start. - `assertThat(context).hasNotFailed()` — asserts it started cleanly (useful to be explicit). - `assertThat(context).getFailure()` — returns the captured `Throwable` and returns an AssertJ `ThrowableAssert`, so you can chain: `.hasRootCauseInstanceOf(...)`, `.hasMessageContaining(...)`, `.rootCause().hasMessageContaining(...)`. ```java @Test void failsWhenRequiredPropertyMissing() { new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(MyStrictAutoConfiguration.class)) .run(ctx -> assertThat(ctx) .hasFailed() .getFailure() .hasRootCauseInstanceOf(IllegalStateException.class) .hasMessageContaining("my.required.key")); } ``` ### Gotcha: don't just call getBean and expect a throw If you call `assertThat(ctx).getBean(Foo.class)` on a *failed* context, behaviour is about the failure state; the idiomatic path for failure tests is `hasFailed().getFailure()`, not fishing for beans. And do assert `hasFailed()` first — an assertion like `getFailure()` on a context that *didn't* fail is a test smell. ## Common failure scenarios worth testing - A `@ConfigurationProperties` binding with `@Validated` that rejects bad values → expect `hasFailed()` with a `BindValidationException`/`ConstraintViolation` in the cause chain. - An auto-config that requires a bean the user didn't supply and has no default → `NoSuchBeanDefinitionException` root cause. - Fail-fast startup checks (`InitializingBean#afterPropertiesSet` throwing) → assert the message. ## When to prefer the runner vs @SpringBootTest **Prefer `ApplicationContextRunner`:** - Testing auto-configuration classes and `@Conditional*` outcomes. - Many permutations of properties / classpath (`FilteredClassLoader`) / user beans. - Verifying graceful back-off and clear failure messages. - Speed and isolation matter (it's milliseconds; no scan, no server). **Prefer `@SpringBootTest` (or slices `@WebMvcTest`, `@DataJpaTest`):** - You need the *whole* application wired as it runs in production. - You need a real (or mock) servlet/reactive server and to exercise HTTP endpoints. - Integration behaviour across many collaborating beans, DB, transactions. - End-to-end confidence rather than unit-level condition verification. The two are complementary: use the runner to lock down your auto-config's conditional matrix cheaply, and a small number of `@SpringBootTest`s to prove the assembled app actually boots and serves. ## Design intent The runner embodies 'test the configuration logic, not the framework': fast, deterministic, per-run isolation, capture-don't-throw failures. That's why Spring Boot's own auto-configuration test suite is built almost entirely on it.

  • If a context fails to start, does run() throw the exception out to the test method?
    No — the runner captures it and provides a failed AssertableApplicationContext. You assert with hasFailed() and inspect it via getFailure(). This lets you write clean negative tests without try/catch.
  • Give one thing you'd test with @SpringBootTest that the runner can't cover.
    That the fully component-scanned application boots and serves a real HTTP endpoint end-to-end (e.g. via TestRestTemplate against an embedded server) — the runner starts no server and doesn't scan your whole app.

saying these in an interview costs you the question

  • Expecting run() to throw on startup failure and wrapping it in try/catch
  • Calling getFailure() without first asserting hasFailed()
  • Using the runner to try to test real HTTP endpoints / full app boot
  • Believing @SpringBootTest is always the right tool even for pure condition testing (slow, over-broad)

context