skip to content

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?

level: principalimportance: nice to knowfreq 20%

answer

  1. centralize via base class or MockMvcBuilderCustomizer
  2. apply(springSecurity()) + alwaysDo(log()) as defaults
  3. custom ResultHandler = any MvcResult lambda
  4. keep alwaysExpect minimal; scope per @WebMvcTest slice
  5. DRY vs hidden-behavior; can't always* an injected MockMvc

basics

~10 s

Build 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 s

I 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 lines
java
abstract 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

for a junior

Understand that shared setup can add logging/security once instead of per test.

for a middle

Build a base class with apply(springSecurity()) + alwaysDo(log()); know the injected-MockMvc limitation.

for a senior

Choose between customizer vs base class, scope invariants per slice, write custom ResultHandlers.

for a principal

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)

context