skip to content

Beyond jsonPath, what body-assertion and result-extraction options does WebTestClient offer, and when would you use expectBody(Class) vs expectBodyList vs returnResult?

level: seniorimportance: should knowfreq 45%

answer

  1. untyped expectBody() → jsonPath / json(...)
  2. expectBody(Class) → isEqualTo / value
  3. expectBodyList(Class) → hasSize / contains
  4. returnResult → value out of chain; Flux for streams
  5. SSE/infinite → returnResult + StepVerifier, not expectBodyList

basics

~20 s

expectBody(Class) decodes one object; expectBodyList(Class) decodes an array to a List; expectBody() (untyped) gives jsonPath/json(...) assertions on raw JSON; returnResult(Class) exits the fluent chain to get the decoded body (a Flux for streaming) for custom assertions.

solid answer

~40 s

After exchange() and status, WebTestClient offers several body paths. expectBody() with no type gives a BodyContentSpec for content assertions: jsonPath("$.x"), json("{...}") for full-document equality, xpath(...), and consumeWith(result -> ...) for raw access. expectBody(User.class) decodes the JSON into a single object exposing isEqualTo(expected) and value(user -> assertThat(...)). expectBody(String.class) yields the raw string. expectBodyList(User.class) decodes a JSON array into a List with hasSize, contains, and value assertions. When you need the actual value out of the chain, returnResult(Class) returns an EntityExchangeResult whose getResponseBody() you assert with any framework; for streaming/reactive responses it returns a FluxExchangeResult<T> whose getResponseBody() is a Flux<T> you verify with StepVerifier. Use jsonPath for partial/loose checks, typed decoding for strong equality, and returnResult when downstream logic needs the captured value.

code

java · 22 lines
java
// Typed single object
client.get().uri("/users/1").exchange()
    .expectStatus().isOk()
    .expectBody(User.class)
        .value(u -> assertThat(u.getName()).isEqualTo("Ada"));

// Typed list
client.get().uri("/users").exchange()
    .expectStatus().isOk()
    .expectBodyList(User.class).hasSize(2);

// Reactive stream (SSE): extract Flux, verify with StepVerifier
FluxExchangeResult<Event> result = client.get().uri("/events")
    .accept(MediaType.TEXT_EVENT_STREAM)
    .exchange()
    .expectStatus().isOk()
    .returnResult(Event.class);

StepVerifier.create(result.getResponseBody())
    .expectNextCount(3)
    .thenCancel()
    .verify();

go deeper

for a junior

Know jsonPath and expectBody(Class) exist for JSON assertions.

for a middle

Choose between untyped jsonPath, typed single, and typed list for a given response.

for a senior

Handle streaming responses with returnResult + StepVerifier and know jsonPath is untyped-only.

for a principal

Set team conventions for loose (jsonPath) vs strict (typed) assertions and reliable streaming-endpoint tests.

## The body-assertion surface Once you reach `.expectStatus()....` you branch into body handling. There are three broad shapes. ### 1. Untyped: `expectBody()` → `BodyContentSpec` No decoding to a Java type; you assert on the raw JSON/text. - `.jsonPath("$.name").isEqualTo("Ada")` — JsonPath expression assertions (`$` root, `$.a.b`, `$.list[0]`, `$.list.length()`), with `.exists()`, `.doesNotExist()`, `.isArray()`, `.isNotEmpty()`, `.value(Matcher)`. - `.json("{\"id\":1,\"name\":\"Ada\"}")` — asserts the **whole body** equals this JSON (lenient by default; strict order-independent field match). - `.xpath("/user/name").isEqualTo("Ada")` — for XML. - `.consumeWith(result -> { ... })` — hand you the `EntityExchangeResult<byte[]>` for arbitrary assertions. - `.isEmpty()` — assert no body. ### 2. Typed single object: `expectBody(Class<T>)` → `BodySpec<T, ?>` Decodes the response body into a `T` using the configured codecs (Jackson etc.). - `.isEqualTo(expectedUser)` — full `equals()` comparison. - `.value(user -> assertThat(user.getName()).isEqualTo("Ada"))` — lambda/consumer assertion. - `.value(User::getName, equalTo("Ada"))` — extractor + Hamcrest matcher. - `.consumeWith(result -> ...)` — access to `EntityExchangeResult<T>`. Use when you want **strong, type-safe equality** on a single resource. ### 3. Typed list: `expectBodyList(Class<T>)` → `ListBodySpec<T>` Decodes a JSON array into `List<T>`. - `.hasSize(3)`, `.contains(u1, u2)`, `.doesNotContain(u3)`, `.value(list -> ...)`. Use for **collection endpoints**. ## Extracting values: `returnResult(Class<T>)` Sometimes you don't want to assert inline — you want the value out of the fluent chain. - Non-streaming: `EntityExchangeResult<T> result = spec.expectBody(User.class).returnResult();` then `result.getResponseBody()` is the decoded `T`; also gives status, headers, request info. - Streaming/reactive: `FluxExchangeResult<Event> result = client.get().uri("/stream").exchange().expectStatus().isOk().returnResult(Event.class);` — `result.getResponseBody()` is a **`Flux<Event>`** you verify with Reactor's `StepVerifier`: ```java StepVerifier.create(result.getResponseBody()) .expectNext(e1, e2) .thenCancel() .verify(); ``` This is the idiomatic way to test **Server-Sent Events / infinite or backpressured streams**, because inline `expectBodyList` would try to collect the whole (possibly unbounded) stream and block. ## Reactive vs non-reactive assertions - For a controller returning `Mono<User>` or a plain `User`, `expectBody(User.class)` works identically — WebTestClient blocks and decodes the single value. - For `Flux<User>` returned as a normal JSON array, `expectBodyList(User.class)` collects and asserts. - For `Flux<User>` streamed as `text/event-stream`, use `returnResult(User.class)` + `StepVerifier` so you control demand and cancellation. ## Gotchas - `expectBody()` (untyped) is where `jsonPath` lives; calling `expectBody(User.class).jsonPath(...)` won't compile — the typed spec has no jsonPath. - `.json(...)` full-document match is order-independent but by default lenient about extra fields only in specific modes — know your comparator when a test surprises you. - Inline `expectBodyList` on an unbounded/SSE stream will hang until the response timeout — switch to `returnResult` + `StepVerifier`. - `returnResult(Class)` on a streaming body starts consuming lazily; the request may not fully complete until you subscribe via StepVerifier. - Decoding uses the same codecs as production; a missing/misconfigured Jackson module makes typed decoding fail even though jsonPath on the raw body still passes.

  • Why not use expectBodyList to test a Server-Sent Events endpoint?
    SSE streams can be unbounded; expectBodyList tries to collect the entire body and blocks until the response timeout. returnResult(Class) gives a Flux you drive with StepVerifier, controlling demand and cancellation.
  • Can you call jsonPath after expectBody(User.class)?
    No. jsonPath lives on the untyped BodyContentSpec from expectBody(). The typed BodySpec exposes isEqualTo/value/consumeWith instead.

saying these in an interview costs you the question

  • Claiming jsonPath is available on the typed expectBody(Class) spec.
  • Using expectBodyList on an infinite/SSE stream and expecting it not to hang.
  • Thinking returnResult performs assertions itself rather than extracting the result.

context