What are WebApplicationContextRunner and ReactiveWebApplicationContextRunner for, and what does the runner's immutability mean for how you reuse it across tests?
answer
- 3 runners: plain / servlet Web / Reactive
- web variant needed for @ConditionalOnWebApplication
- no real server started, just context type
- immutable: with* returns new instance
- shared base field safe; use the return value
basics
~20 sThe Web/Reactive variants build a servlet or reactive web ApplicationContext so you can test conditions like @ConditionalOnWebApplication. The runner is immutable: every with* returns a new instance, so a shared base runner is safe and you must use the returned copy.
solid answer
~40 sThere are three runners: ApplicationContextRunner (a plain non-web AnnotationConfigApplicationContext), WebApplicationContextRunner (a servlet AnnotationConfigServletWebApplicationContext), and ReactiveWebApplicationContextRunner (a reactive context). You pick the web/reactive variant when the auto-configuration you're testing is gated on the context type — e.g. @ConditionalOnWebApplication(type = SERVLET) or reactive-only beans — because only the matching runner presents the right context type to those conditions. None of them starts a real embedded server; they just create the appropriate context flavor. Immutability is a core design property: the runner is thread-safe and every with* method (withConfiguration, withPropertyValues, withBean, withClassLoader) returns a brand-new configured instance, leaving the original untouched. So you can hold a shared final field with common setup and derive per-test variations from it without cross-contamination — but you must use the returned instance, or the extra configuration is silently lost.
code
java · 13 linesimport org.springframework.boot.test.context.runner.WebApplicationContextRunner;
@Test
void registersFilterOnlyInServletWebContext() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(MyWebAutoConfiguration.class))
.run(ctx -> assertThat(ctx).hasSingleBean(MyServletFilter.class));
}
// Reusing an immutable base without cross-contamination:
private final ApplicationContextRunner base = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(MyAutoConfiguration.class));
// base.withPropertyValues("x=1") returns a NEW runner; base is unchanged.go deeper
Knows the plain runner; may not know the web/reactive variants exist.
Picks the right variant for context-type conditions and understands the immutability/reuse rule.
Explains that no server is started and why a shared immutable base is the idiomatic pattern.
Contrasts with @SpringBootTest webEnvironment and reasons about test-suite structure using composable immutable bases.
## The three runners All live in `org.springframework.boot.test.context.runner`: 1. `ApplicationContextRunner` → backs an `AnnotationConfigApplicationContext` (non-web). Default choice. 2. `WebApplicationContextRunner` → backs a servlet web context (`AnnotationConfigServletWebApplicationContext`). 3. `ReactiveWebApplicationContextRunner` → backs a reactive web context (`AnnotationConfigReactiveWebApplicationContext`). They share the same fluent API (`AbstractApplicationContextRunner`), so the only difference is the *type* of context created. ## Why the web/reactive variants exist Many auto-configuration conditions key on the application type: - `@ConditionalOnWebApplication(type = Type.SERVLET)` — only in a servlet web context. - `@ConditionalOnWebApplication(type = Type.REACTIVE)` — only reactive. - `@ConditionalOnNotWebApplication` — only when not web. If you test a servlet-only bean with the plain `ApplicationContextRunner`, `@ConditionalOnWebApplication(SERVLET)` sees a non-web context and backs off — your bean never appears and the test is meaningless. Use `WebApplicationContextRunner` so the condition sees a servlet context. ```java new WebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(MyWebAutoConfiguration.class)) .run(ctx -> assertThat(ctx).hasSingleBean(MyServletFilter.class)); ``` Important: **no real HTTP server is started**. The runner only creates a context whose type reports as web/reactive; it does not bind a port or run Tomcat/Netty. For real request testing you'd use `@WebMvcTest`/`@SpringBootTest(webEnvironment=...)`, which is a different tool. ## Immutability and reuse Every runner extends `AbstractApplicationContextRunner`, which is **immutable**. Each `with*` call constructs a new instance carrying the accumulated configuration and returns it; the receiver is unchanged. Consequences: - **Thread-safe / shareable.** You can keep a `private final ApplicationContextRunner runner = new ApplicationContextRunner().withConfiguration(...)` field and every test derives its own variant without stepping on others. - **You must use the return value.** `runner.withPropertyValues("x=1");` on its own line does nothing to `runner`; you need to chain it into `.run(...)` or reassign. This is the single most common bug. - **Composable base setups.** Build a base runner with the common auto-config, then layer per-test properties/classloaders on top — each test's chain is independent. ```java private final ApplicationContextRunner base = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(MyAutoConfiguration.class)); @Test void a() { base.withPropertyValues("m.enabled=true").run(ctx -> ...); } @Test void b() { base.withPropertyValues("m.enabled=false").run(ctx -> ...); } // base is never mutated; a() and b() don't affect each other. ``` ## Summary decision Pick the runner by the context type the conditions expect; rely on immutability to share a base configuration cleanly and remember to consume the returned instance.
- Does WebApplicationContextRunner start an embedded Tomcat so you can send real HTTP requests?No. It only creates a servlet-type ApplicationContext so context-type conditions evaluate correctly. It binds no port and starts no server; for real requests use @WebMvcTest or @SpringBootTest with a web environment.
- Why is it safe to declare the runner as a shared final field across many @Test methods?Because it's immutable — every with* returns a new instance and each run() builds a fresh isolated context. No test can mutate the shared base or leak state into another.
saying these in an interview costs you the question
- Thinking the Web variant starts a real embedded server
- Testing @ConditionalOnWebApplication(SERVLET) beans with the plain runner
- Calling with* on its own line and expecting the base runner to change
- Assuming a shared runner field is unsafe across tests (it's immutable, so it's fine)