skip to content

How does Spring REST Docs work with WebTestClient for a reactive/WebFlux application, and how does it differ from the MockMvc integration?

level: seniorimportance: should knowfreq 28%

answer

  1. spring-restdocs-webtestclient module
  2. WebTestClientRestDocumentation.documentationConfiguration = filter
  3. .consumeWith(document(...)) not .andDo(...)
  4. set baseUrl / modifyUris (no servlet Host)
  5. same descriptors, snippets, strict validation

basics

~10 s

Use the spring-restdocs-webtestclient module. Configure WebTestClient with WebTestClientRestDocumentation.documentationConfiguration, then call .consumeWith(WebTestClientRestDocumentation.document("id", ...)) on the response. Same snippets and descriptors as MockMvc, just a different entry-point class.

solid answer

~30 s

For WebFlux (or any WebTestClient-based test) you swap the module and the documentation class. Add spring-restdocs-webtestclient instead of spring-restdocs-mockmvc. You build the client with WebTestClient.bindToApplicationContext(context).configureClient().filter(WebTestClientRestDocumentation.documentationConfiguration(restDocumentation)).build(), or with @AutoConfigureRestDocs + @AutoConfigureWebTestClient in a Boot test which wires it for you. Documentation attaches at the assertion terminal: client.get().uri("/users/{id}", 1).exchange().expectStatus().isOk().expectBody().consumeWith(WebTestClientRestDocumentation.document("users-get", responseFields(...))). The snippet types, field descriptors, preprocessors and strict validation are identical to MockMvc — only the harness differs. A WebTestClient nuance: you should set a base URI (e.g. .baseUrl("https://api.example.com")) or use modifyUris, since there's no servlet Host to infer from.

code

java · 31 lines
java
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;
import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.documentationConfiguration;
import static org.springframework.restdocs.payload.PayloadDocumentation.*;

@ExtendWith(RestDocumentationExtension.class)
@SpringBootTest
class UserHandlerDocsTest {

    WebTestClient client;

    @BeforeEach
    void setUp(ApplicationContext context, RestDocumentationContextProvider restDocs) {
        this.client = WebTestClient.bindToApplicationContext(context)
            .configureClient()
            .baseUrl("https://api.example.com")
            .filter(documentationConfiguration(restDocs))
            .build();
    }

    @Test
    void getUser() {
        client.get().uri("/users/{id}", 1)
            .exchange()
            .expectStatus().isOk()
            .expectBody()
            .consumeWith(document("users-get",
                responseFields(
                    fieldWithPath("id").description("Unique id"),
                    fieldWithPath("name").description("Display name"))));
    }
}

go deeper

for a junior

Know WebTestClient uses a different module and .consumeWith(document(...)).

for a middle

Wire the documentationConfiguration filter and reuse the same responseFields/preprocessors as MockMvc.

for a senior

Explain the baseUrl/modifyUris need, bindToApplicationContext vs bindToServer, and that WebTestClient covers both WebFlux and MVC.

for a principal

Choose the harness per stack (WebFlux -> WebTestClient) and keep one descriptor/preprocessor convention across MockMvc and WebTestClient tests.

**Three supported test harnesses.** Spring REST Docs is harness-agnostic: the same snippet engine is fed by one of three integration modules — `spring-restdocs-mockmvc` (Servlet MVC via MockMvc), `spring-restdocs-webtestclient` (reactive WebFlux and also MVC via `WebTestClient`), and `spring-restdocs-restassured` (black-box against a running server). This leaf focuses on MockMvc and WebTestClient. **WebTestClient setup (manual).** ```java @ExtendWith(RestDocumentationExtension.class) class Docs { WebTestClient client; @BeforeEach void setup(ApplicationContext ctx, RestDocumentationContextProvider rd) { this.client = WebTestClient.bindToApplicationContext(ctx) .configureClient() .baseUrl("https://api.example.com") .filter(WebTestClientRestDocumentation.documentationConfiguration(rd)) .build(); } } ``` The `documentationConfiguration(...)` is an `ExchangeFilterFunction` that captures each exchange. `WebTestClientRestDocumentation` lives in `org.springframework.restdocs.webtestclient`. **Boot slice.** `@AutoConfigureRestDocs` combined with `@AutoConfigureWebTestClient` (or a full `@SpringBootTest(webEnvironment = RANDOM_PORT)` with an injected `WebTestClient`) auto-configures the filter, so you just inject `WebTestClient` and call `document(...)`. **Where documentation attaches.** Unlike MockMvc's `.andDo(document(...))` `ResultHandler`, WebTestClient uses the body-consumer terminal: ```java client.get().uri("/users/{id}", 1) .exchange() .expectStatus().isOk() .expectBody() .consumeWith(document("users-get", responseFields(fieldWithPath("id").description("id")))); ``` `document(...)` here returns a `Consumer<EntityExchangeResult<byte[]>>`. Everything downstream — `responseFields`, `requestFields`, `pathParameters`, `preprocessRequest/Response`, strict validation — is the *same* descriptor API imported from the same `payload`/`request`/`operation.preprocess` packages. **Key differences vs MockMvc.** - Different module + different documentation class (`WebTestClientRestDocumentation` vs `MockMvcRestDocumentation`). - Attachment point: `.consumeWith(document(...))` on the body vs `.andDo(document(...))` on the result. - Path templates: use `.uri("/users/{id}", 1)` so `pathParameters` can resolve the template. - **Base URI**: WebTestClient has no servlet request Host, so set `.baseUrl(...)` (or use `modifyUris()` preprocessor / `@AutoConfigureRestDocs` URI attributes) or documented curl/http-request snippets show a placeholder host. - WebTestClient can drive *both* WebFlux and MVC apps; MockMvc is Servlet-only. **Gotchas.** - Forgetting the `filter(documentationConfiguration(...))` means `document(...)` throws because no context was captured. - With `bindToApplicationContext`/`bindToController` the calls are in-process (no network) — like MockMvc; with `bindToServer` it's a real HTTP client (closer to REST Assured). - Reactive streaming responses are captured as the fully materialized body for documentation. **When to use.** Use the WebTestClient integration for WebFlux endpoints or when your team already standardizes on WebTestClient for controller tests, so docs come from the same tests you already write.

  • What is the attachment point for document() with WebTestClient, and how does it differ from MockMvc?
    With WebTestClient you call .consumeWith(document(...)) on expectBody() — document returns a Consumer of the exchange result. MockMvc uses .andDo(document(...)) as a ResultHandler. The descriptor/snippet APIs are otherwise identical.
  • Why do you often need to set a baseUrl or modifyUris with WebTestClient but rarely with MockMvc?
    MockMvc synthesizes a servlet request with localhost, giving REST Docs a Host to render. WebTestClient (bound to context) has no servlet Host, so the documented request URI needs an explicit baseUrl or a modifyUris preprocessor to look production-like.

saying these in an interview costs you the question

  • Using MockMvcRestDocumentation.document() with WebTestClient (wrong module/class)
  • Forgetting the documentationConfiguration filter, so nothing is captured
  • Assuming field descriptors/strict rules differ between the two harnesses (they don't)

context