I am exploring testing HTTP endpoints using Spring WebFlux and came across the doExchange method in WebTestClient. I want to understand what doExchange does, how it differs from other request execution methods, and when it should be used in unit or integration tests.
Having worked quite a bit with WebTestClient over the years, here’s how I usually explain it.
When you call doexchange, you’re essentially triggering the request at a lower level than the usual exchange() or expect*() methods. It simply executes the HTTP call and hands you a raw ClientResponsenothing more, nothing less.
Because it doesn’t apply automatic assertions or response mapping, doexchange gives you full control. That’s why it’s great when you want to inspect headers, cookies, status codes, or manually parse the body instead of relying on the fluent assertion API.
A typical use case looks something like this:
ClientResponse response = webTestClient.get()
.uri("/api/test")
.exchange()
.returnResult(Void.class)
.getResponse();
Whenever I’m writing an integration test and need to handle the response programmatically rather than assert immediately, doexchange tends to be the right fit
Jumping in here I’ve had similar experiences, and @tim-khorev spot on. To add a bit more context, doexchange tends to shine in more advanced or internal scenarios. For most everyday unit tests, you’ll probably stick with exchange(), expectStatus(), or expectBody() since they’re much more readable and intention-revealing.
But when you’re dealing with scenarios like custom HTTP headers, cookies, streaming bodies, or anything that isn’t neatly handled by the fluent API, doexchange becomes your escape hatch. It basically says: “Here’s the raw ClientResponse do whatever you need before asserting.”
So I’d say: default to the higher-level methods, but keep doexchange in your toolbox for those more intricate test flows.
Adding on to both points one thing I’ve found especially helpful is using doexchange when working with reactive pipelines. If you’re testing endpoints that return a Flux or Mono, the raw access helps you manipulate the stream before making any assertions.
For example:
webTestClient.get().uri("/flux")
.exchange()
.returnResult(String.class)
.getResponseBody()
.collectList()
.block();
This pattern gives you far more flexibility than jumping straight into expectBody(). So whenever your test needs to transform, collect, or inspect the reactive body in a custom way, doexchange really comes in handy.
But if all you need is a quick status or body check, the higher-level expect methods are still cleaner.