How do you assert on a context that fails to start with ApplicationContextRunner, and when should you prefer the runner over @SpringBootTest?
answer
- failure captured, not thrown
- hasFailed() / hasNotFailed()
- getFailure() -> AssertJ ThrowableAssert chain
- hasRootCauseInstanceOf + hasMessageContaining
- runner for conditions/perms; @SpringBootTest for full app + server
basics
~10 sInside 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 sWhen 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@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
May not know failures are captured; likely expects an exception to be thrown.
Uses hasFailed()/getFailure() for negative tests and knows the runner is faster than @SpringBootTest.
Chains ThrowableAssert on getFailure() and articulates the runner-vs-@SpringBootTest trade-off precisely.
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)