skip to content

What is WebTestClient and how do you use it to assert an HTTP endpoint's status and JSON body?

level: juniorimportance: must knowfreq 65%

answer

  1. build → exchange() → assert
  2. expectStatus().isOk()
  3. expectBody().jsonPath("$.field")
  4. expectBody(Class) / expectBodyList(Class)
  5. blocks with timeout, but tests WebFlux + MVC

basics

~10 s

WebTestClient is Spring's fluent HTTP test client. You call an endpoint, then chain assertions: get().uri(...).exchange().expectStatus().isOk().expectBody().jsonPath("$.name").isEqualTo("Ada").

solid answer

~40 s

WebTestClient is a non-blocking, fluent client from spring-test built for testing web endpoints. You build a request (get()/post()/etc, uri(), headers, body), call exchange() to perform it, then chain assertions on the response. expectStatus() checks the HTTP status (isOk(), isCreated(), isBadRequest(), isEqualTo(int)); expectBody() enters body assertions where you can use jsonPath("$.field") to assert JSON values, decode to a class with expectBody(Foo.class), or expectBodyList(Foo.class) for arrays. It works against both a real running server and a mock server bound to controllers or an application context, and it drives both Spring WebFlux and (since Spring 5.3/Boot 2.4+) Spring MVC endpoints. Internally it blocks with a timeout so tests read synchronously even though the underlying exchange is reactive.

code

java · 9 lines
java
webTestClient.get().uri("/users/{id}", 1)
    .accept(MediaType.APPLICATION_JSON)
    .exchange()
    .expectStatus().isOk()
    .expectHeader().contentType(MediaType.APPLICATION_JSON)
    .expectBody()
        .jsonPath("$.id").isEqualTo(1)
        .jsonPath("$.name").isEqualTo("Ada")
        .jsonPath("$.roles").isArray();

go deeper

for a junior

Know the request → exchange() → expectStatus/expectBody/jsonPath chain and that $ is the JSON root.

for a middle

Distinguish expectBody() vs expectBody(Class) vs expectBodyList(Class) and know the default 5s timeout.

for a senior

Explain the blocking-with-timeout model and returnResult for streaming, and that it drives MVC and WebFlux.

for a principal

Position WebTestClient in the team's testing strategy versus MockMvc and full end-to-end tests.

## What WebTestClient is `WebTestClient` (package `org.springframework.test.web.reactive.server`) is a **fluent, non-blocking HTTP client designed for tests**. It wraps Spring's reactive `WebClient` and adds assertion methods so you can exercise a web endpoint and verify the response in one readable chain. Despite living in the `reactive` package, it can test **both** Spring WebFlux and Spring MVC controllers. ## The request → exchange → assert flow Every test follows three phases: 1. **Build the request** — pick an HTTP method (`get()`, `post()`, `put()`, `delete()`, `patch()`), set `.uri("/users/{id}", 1)`, optionally `.header(...)`, `.accept(MediaType.APPLICATION_JSON)`, `.bodyValue(payload)` or `.body(publisher, Class)`. 2. **Perform it** — `.exchange()` actually sends the request and returns a `WebTestClient.ResponseSpec`. Nothing happens until you call `exchange()`. 3. **Assert** — chain assertions on the `ResponseSpec`. ## Status assertions — `expectStatus()` `.expectStatus()` returns a `StatusAssertions` with methods like: - `.isOk()` (200), `.isCreated()` (201), `.isNoContent()` (204) - `.isBadRequest()` (400), `.isUnauthorized()` (401), `.isForbidden()` (403), `.isNotFound()` (404) - `.is4xxClientError()`, `.is5xxServerError()`, `.is2xxSuccessful()` - `.isEqualTo(HttpStatus.CONFLICT)` or `.isEqualTo(409)` for any specific code ## Body assertions — `expectBody()` After status you typically assert the body. Several forms exist: - `.expectBody()` — a generic `BodyContentSpec` for raw/JSON assertions without decoding to a type. - `.expectBody(User.class)` — decodes the JSON into a `User` and gives you `.isEqualTo(expected)` or `.value(user -> assertThat(...))`. - `.expectBodyList(User.class)` — decodes a JSON array into a `List<User>` with `.hasSize(3)`, `.contains(...)`. - `.expectBody(String.class)` — the raw body as a String. ### `jsonPath` `.expectBody().jsonPath("$.name").isEqualTo("Ada")` uses the **JsonPath** library (`$` = document root, `$.name` = the `name` field, `$.items[0].id` = nested/array access). Assertion methods include `.isEqualTo(...)`, `.exists()`, `.doesNotExist()`, `.isNotEmpty()`, `.isArray()`, `.isBoolean()`, `.value(matcher)` for a Hamcrest matcher. You can also assert whole documents with `.json("{...}")` or `.xpath(...)` for XML. ## Complete example ```java webTestClient.get().uri("/users/1") .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk() .expectHeader().contentType(MediaType.APPLICATION_JSON) .expectBody() .jsonPath("$.id").isEqualTo(1) .jsonPath("$.name").isEqualTo("Ada"); ``` ## Reactive vs non-reactive The endpoint under test can return a plain object, a `Mono<T>`, or a `Flux<T>` — WebTestClient handles all of them. On the test side WebTestClient **blocks with a configurable timeout** (default 5s, set via `.responseTimeout(Duration)`), so your test code stays synchronous and readable even though the underlying exchange is reactive. For streaming responses you can drop to `.returnResult(Foo.class)` and get a `FluxExchangeResult` whose `getResponseBody()` is a `Flux<Foo>` you verify with `StepVerifier`. ## When to use Reach for WebTestClient whenever you want to test a controller through the real HTTP/serialization stack — either fast (mock server, no socket) or end-to-end (running server). It is the recommended client for WebFlux and a modern alternative to MockMvc for MVC. ## Gotchas - You must call `exchange()`; forgetting it means no request is sent. - `expectBody()` (no type) vs `expectBody(Class)` behave differently — jsonPath lives on the untyped/`BodyContentSpec` form. - The default 5s response timeout can cause confusing failures on slow endpoints — raise it explicitly.

  • What does exchange() return and why must you call it?
    It performs the request and returns a ResponseSpec on which status/body/header assertions are chained. Without exchange() the request is never sent, so the chain does nothing.
  • How do you assert a JSON array field is present and has three elements?
    expectBody().jsonPath("$.items").isArray() plus expectBodyList(Item.class).hasSize(3), or jsonPath("$.items.length()").isEqualTo(3).

saying these in an interview costs you the question

  • Thinking WebTestClient only works for WebFlux and never for Spring MVC.
  • Believing you assert directly after uri() without calling exchange().
  • Confusing jsonPath's $ root syntax with a Spring-specific DSL.

context