How do you decide between @WebFluxTest/@RestClientTest slices and a full @SpringBootTest, and what are the tradeoffs?
answer
- Pyramid: many slices, few full-context, minimal e2e
- Slice = fast + isolated but mocks may drift
- @SpringBootTest = real wiring, security, DB, transport
- RANDOM_PORT + bindToServer for real HTTP
- Context caching: consistent config = reuse; @DirtiesContext evicts
basics
~20 sUse slices (@WebFluxTest, @RestClientTest) for fast, focused unit-ish tests of one layer with collaborators mocked. Use @SpringBootTest when you need the real wired application — cross-layer flows, real security, real transport, or integration with the database or a running server. Slices are cheaper but cover less.
solid answer
~40 sThink in a testing pyramid. **Slices** (`@WebFluxTest`, `@RestClientTest`, `@DataR2dbcTest`, etc.) start a **minimal context** for one concern, so they're fast, isolate failures, and encourage mocking collaborators — ideal for verifying controller routing/validation/serialization or an HTTP client's request/response handling. Their limit: they don't exercise how layers wire together, real security transport, or persistence. **`@SpringBootTest`** loads the **full ApplicationContext** and, with `webEnvironment=RANDOM_PORT`, a real server + `WebTestClient.bindToServer()` — use it for end-to-end flows, cross-module contracts, real filters/TLS, and DB integration (often with Testcontainers). Cost: slower startup, more shared context caching pressure, broader blast radius when something breaks. Strategy: many slice/unit tests, fewer @SpringBootTest integration tests, a handful of true e2e. Also leverage Spring's **context caching** — keep configurations consistent so contexts are reused across tests to control suite time.
code
java · 23 lines// Full end-to-end: real server + real transport + Testcontainers DB.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class CheckoutFlowIT {
@Container
static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16");
@DynamicPropertySource
static void props(DynamicPropertyRegistry r) {
r.add("spring.r2dbc.url", () -> "r2dbc:postgresql://" + db.getHost()
+ ":" + db.getFirstMappedPort() + "/" + db.getDatabaseName());
}
@Autowired WebTestClient client; // bound to the real random port
@Test
void placesOrderThroughAllLayers() {
client.post().uri("/orders").bodyValue(new OrderRequest("sku-1", 2))
.exchange()
.expectStatus().isCreated();
}
}go deeper
Know slices are faster and narrower; @SpringBootTest loads everything.
Pick the right slice per layer and use @SpringBootTest for integration flows.
Reason about mock drift, RANDOM_PORT vs in-memory, and Testcontainers for DB integration.
Own the testing pyramid, context-cache economics, and escalation from stubs to contract/e2e testing as team policy.
## The core tradeoff Every Spring test either loads a **narrow** context (a *slice*) or a **broad** one (`@SpringBootTest`). Narrow = fast, isolated, but low integration fidelity. Broad = slow, but exercises real wiring. Good test strategy places most tests at the narrow end and reserves broad tests for genuine integration risk. ## Slice tests Slices are `@...Test` annotations that use Spring Boot's **auto-configuration filtering** to include only the beans for one layer: - **`@WebFluxTest`** — reactive controllers + WebTestClient; mock services. - **`@RestClientTest`** — an HTTP client + MockRestServiceServer; mock the network. - Others: `@WebMvcTest`, `@DataJpaTest`, `@DataR2dbcTest`, `@JsonTest`. Strengths: **speed** (tiny context), **precise failure localization** (a failing @WebFluxTest points at the controller), **forces good design** (you must mock collaborators, revealing coupling). Weaknesses: **no cross-layer coverage** — you can pass a @WebFluxTest and a @DataR2dbcTest yet have a broken end-to-end path because the mock didn't reflect reality; **security/transport gaps** (in-memory binding skips real server filters, TLS, actual ports); **mock drift** (stubs diverge from real collaborators). ## @SpringBootTest `@SpringBootTest` boots the **entire application context**. Options via `webEnvironment`: - `MOCK` (default) — mock servlet/reactive environment, pair with `@AutoConfigureWebTestClient` for in-memory WebTestClient across the full context. - `RANDOM_PORT` / `DEFINED_PORT` — a **real embedded server**; use `WebTestClient.bindToServer().baseUrl(...)` (injected) to exercise the real transport. - `NONE` — no web environment (for non-web integration). Use it for: multi-layer flows (controller → service → repository), real Spring Security end-to-end, real HTTP transport concerns, database integration (commonly **Testcontainers** for a real Postgres/etc.), and Spring Modulith module-interaction tests. Costs: **slow startup**, heavier memory, and pressure on the **context cache** — Spring caches ApplicationContexts keyed by configuration; every unique config (different `@MockBean` sets, properties, active profiles) creates a **new cached context**, and too many distinct contexts blow up suite time. Keep configurations consistent to maximize reuse. ## Decision heuristics - Testing **routing/validation/serialization/error mapping** of one controller → `@WebFluxTest`. - Testing **an outbound HTTP client's** request building + response/error parsing → `@RestClientTest`. - Testing **persistence queries/mappings** → `@DataR2dbcTest`/`@DataJpaTest`. - Testing **a full user flow, real authz, real DB, or transport** → `@SpringBootTest` (+ RANDOM_PORT / Testcontainers). - Contract with an external provider → **contract tests** (Spring Cloud Contract) or **WireMock**, beyond MockRestServiceServer's in-JVM stubs. ## Gotchas at scale - **Mock drift** in slices: pair heavy slice usage with a few real integration tests so mocks don't hide contract breaks. - **Context cache thrash**: minimize distinct context configs; avoid sprinkling `@MockBean` in ways that fragment caching. - **@DirtiesContext** forces context eviction/rebuild — use sparingly; it defeats caching. - **Slice ≠ security fidelity**: in-memory bindings skip server-added behavior; verify security at least once end-to-end. - **MockRestServiceServer is in-JVM**: it doesn't validate real serialization over the wire, TLS, timeouts, or connection pooling — use WireMock/Testcontainers when those matter. ## Governance angle (principal) Define the team's pyramid: slice+unit as the default, `@SpringBootTest` reserved for integration risk, e2e minimal. Track context-cache reuse and suite time as first-class CI metrics; standardize test security helpers; decide when to escalate from MockRestServiceServer to WireMock/contract testing.
- Why can too many @SpringBootTest classes with different @MockBean sets slow the whole suite?Spring caches ApplicationContexts keyed by their configuration. Each distinct set of mocks/properties/profiles produces a new cache key, so the framework builds and holds many separate contexts instead of reusing one — startup time multiplies. Keeping configs consistent maximizes cache reuse.
- When is MockRestServiceServer the wrong tool and WireMock/contract testing right?When you need real over-the-wire fidelity — actual serialization across a socket, TLS, timeouts, connection pooling, or provider-driven contract verification. MockRestServiceServer stubs in-JVM at the request factory, so it can't validate those; WireMock (real HTTP) or Spring Cloud Contract fit better.
saying these in an interview costs you the question
- Using @SpringBootTest for everything (slow, poor failure isolation)
- Believing passing slice tests guarantees the end-to-end path works
- Ignoring context caching / overusing @DirtiesContext
- Thinking MockRestServiceServer validates real wire serialization/TLS