How would you standardize MockMvc debugging and security across a large test suite using result handlers, `alwaysDo`, and `apply(springSecurity())` — and what are the trade-offs?
answer
- centralize via base class or MockMvcBuilderCustomizer
- apply(springSecurity()) + alwaysDo(log()) as defaults
- custom ResultHandler = any MvcResult lambda
- keep alwaysExpect minimal; scope per @WebMvcTest slice
- DRY vs hidden-behavior; can't always* an injected MockMvc
basics
~10 sBuild a shared MockMvc via a base class or MockMvcBuilderCustomizer: apply(springSecurity()) for security plus alwaysDo(log()) for uniform, DEBUG-gated diagnostics. Keep global alwaysExpect minimal and add custom ResultHandlers only for cross-cutting captures.
solid answer
~50 sI centralize MockMvc construction so every web test inherits the same behavior. Under Spring Boot I register a `MockMvcBuilderCustomizer` (or build manually in a base class) that calls `apply(springSecurity())` to wire the security filter chain and `alwaysDo(log())` so every request is dumped at DEBUG — silent normally, turn-on-able in CI via log level, no code edits. I avoid `alwaysDo(print())` (unconditional System.out noise) and keep `alwaysExpect(...)` to true universals only, since global matchers break on 204/error/non-JSON responses. For cross-cutting capture I write a small custom `ResultHandler` (`result -> ...`) — e.g. record slow requests or assert a mandatory security header — since `andDo` accepts any `ResultHandler`. The trade-off is coupling: a shared builder is one place to change but can hide per-test intent and impose invariants that don't universally hold, so I scope instances per module where behavior diverges.
code
java · 23 linesabstract class AbstractWebTest {
protected MockMvc mockMvc;
@BeforeEach
void baseSetUp(WebApplicationContext context) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply(springSecurity()) // real security in every subclass
.alwaysDo(log()) // DEBUG-gated dump on every request
.alwaysDo(slowRequestReporter()) // custom cross-cutting ResultHandler
.build();
}
// custom ResultHandler: any lambda over MvcResult
private static ResultHandler slowRequestReporter() {
return result -> {
long ms = /* measured elsewhere */ 0L;
if (ms > 500) {
LoggerFactory.getLogger(AbstractWebTest.class)
.warn("slow endpoint {} took {}ms", result.getRequest().getRequestURI(), ms);
}
};
}
}go deeper
Understand that shared setup can add logging/security once instead of per test.
Build a base class with apply(springSecurity()) + alwaysDo(log()); know the injected-MockMvc limitation.
Choose between customizer vs base class, scope invariants per slice, write custom ResultHandlers.
Own the suite-wide policy: balance DRY vs. hidden behavior, keep globals universal, and ensure slices exercise real security rather than stubs.
**Goal.** In a large suite you want consistent security wiring and diagnostics without copy-pasting `.andDo(...)`, `.apply(...)`, and boilerplate into every test. Result handlers and the builder's `always*`/`apply` hooks make this possible; the engineering judgment is about *how much* to globalize. **Centralization options.** 1. **`MockMvcBuilderCustomizer` bean** — under `@SpringBootTest` + `@AutoConfigureMockMvc`, Boot builds MockMvc for you; you influence it by exposing a `MockMvcBuilderCustomizer` (or `@AutoConfigureMockMvc` already applies `springSecurity()`). This customizer can call `alwaysDo(log())` and register cross-cutting handlers, applied to the auto-configured instance. 2. **Abstract base test class** — build MockMvc manually in `@BeforeEach` via `MockMvcBuilders.webAppContextSetup(context).apply(springSecurity()).alwaysDo(log())...build()`, and have web tests extend it. More explicit, less Boot magic, easy to see. **The pieces.** - **`apply(springSecurity())`** (`SecurityMockMvcConfigurers`) — installs the `FilterChainProxy` and postprocessor infra so real authz runs and `.with(user()/csrf()/jwt())` work. Boot auto-applies it under `@AutoConfigureMockMvc`; do it explicitly for manual builds. - **`alwaysDo(log())`** — a global `ResultHandler` (`MockMvcResultHandlers.log()`) dumping each `MvcResult` at DEBUG under `org.springframework.test.web.servlet.result`. Commit-safe: silent at INFO, enable via `logging.level...=DEBUG` in CI to diagnose a flaky run without touching code. - **Custom `ResultHandler`** — `andDo`/`alwaysDo` accept any `result -> {...}` lambda over `MvcResult`. Uses: capture the response body to a report, log requests exceeding a latency budget, or enforce a mandatory header. This is where you extend beyond `print()`/`log()`. - **`alwaysExpect(...)`** — a global `ResultMatcher`. Powerful but dangerous at scale: an over-broad content-type/status invariant fails on 204, redirects, error `application/problem+json`, or file downloads. Reserve for genuine universals, or scope per-controller MockMvc instances. **Trade-offs / judgment.** - **DRY vs. clarity:** a shared builder removes repetition but hides behavior; a newcomer may not realize security is on or that every request is logged. Document it in the base class. - **Global invariants are brittle:** the more endpoints one MockMvc instance serves, the less likely any `alwaysExpect` holds for all of them. Prefer narrow, per-slice instances (`@WebMvcTest(controllers = X.class)`) where an invariant is actually true. - **Noise vs. observability:** `alwaysDo(print())` guarantees output but floods logs and can interleave under parallel execution; `alwaysDo(log())` gives on-demand, framework-managed output — the better default. - **Slice fidelity:** `standaloneSetup` won't load your real `SecurityFilterChain`; if security correctness matters, use `webAppContextSetup`/`@WebMvcTest` so `apply(springSecurity())` exercises real rules rather than a stub. - **Auto-config boundary:** you can't call `alwaysDo`/`alwaysExpect` on an already-built injected MockMvc; you must go through a `MockMvcBuilderCustomizer` or build manually — a real constraint when standardizing. **Recommended default.** Base class or customizer that does `apply(springSecurity())` (or rely on Boot) + `alwaysDo(log())`; zero or one carefully-chosen `alwaysExpect`; per-controller slices where invariants diverge; custom `ResultHandler`s for genuine cross-cutting capture.
- Why not just put `alwaysExpect(status().is2xxSuccessful())` in the shared base class?Because negative-path tests deliberately assert 4xx/5xx, and many endpoints legitimately return 204/302/error codes. A global success matcher would fail those. Global matchers must be true universals; status is almost never one.
- How do you apply these defaults when using Boot's injected `@AutoConfigureMockMvc` MockMvc?Expose a MockMvcBuilderCustomizer bean; Boot applies it while building the instance (springSecurity() is already auto-applied). You cannot call alwaysDo/alwaysExpect on the finished MockMvc object directly.
- What is the downside of `alwaysDo(print())` at scale?It writes unconditionally to System.out for every request, producing huge, interleaved output under parallel execution and no way to silence it via config. alwaysDo(log()) is DEBUG-gated and framework-managed, so it is the better default.
saying these in an interview costs you the question
- Putting broad status/content-type invariants in a suite-wide alwaysExpect
- Using standaloneSetup + springSecurity() and expecting real security rules
- Trying to call always* on the injected @AutoConfigureMockMvc MockMvc
- Defaulting to alwaysDo(print()) instead of log() for large suites
- Assuming a custom ResultHandler can assert and fail like a ResultMatcher (it can throw, but semantics differ from andExpect)