Back to CI/CD Pipeline
Q27Question 27 of 56

When to use OkHttp MockWebServer versus WebTestClient: testing outbound HTTP calls versus testing your own reactive controllers

Key Concepts
  • MockWebServer tests outbound calls your service makes to downstream services; WebTestClient tests inbound requests your own controllers receive.
  • MockWebServer is a real HTTP server on a local port: you enqueue scripted responses and assert the exact request your code sent.
  • MockWebServer shines at simulating downstream failure: 500s for retry logic, 404s for exception mapping, delays for timeout verification.
  • WebTestClient with @WebFluxTest loads only the web layer, mocks service collaborators, and verifies routing, validation, and response serialization.
  • The two are complementary, not competing: use both in the same test suite for different seams.
  • Choosing the wrong tool is a classic mistake: MockWebServer tells you nothing about your routing; WebTestClient tells you nothing about your outbound client.

Definition

MockWebServer tests the outbound calls your service makes to downstream services; WebTestClient tests the inbound requests your own controllers receive.
  • MockWebServer – a real, lightweight HTTP server from OkHttp that binds to a local port; you enqueue scripted responses, point your outbound client at it, and assert the exact request sent.
  • WebTestClient – an in-memory test client for your own inbound endpoints, used with @WebFluxTest and @MockBean to verify routing, validation, and response serialization.
  • Direction rule – if the thing under test calls OUT to another service, use MockWebServer; if the thing under test receives INBOUND requests, use WebTestClient.
  • Complementary – both are used in the same test suite for different seams: MockWebServer at the back door, WebTestClient at the front door.

Think of National Bank of India's payments service as a bank teller. MockWebServer is a fake phone line pretending to be the credit bureau: you listen to exactly what the teller says and control what the bureau replies (testing outgoing calls). WebTestClient is playing the customer who walks up to the teller's counter with a transfer form: you verify the teller validates it, rejects bad forms, and processes good ones (testing incoming requests).

Understanding the Concepts

What decides which tool to use?

The direction of the call is the whole decision. If the thing under test calls OUT to another service, use MockWebServer. If the thing under test receives INBOUND requests, use WebTestClient.

DIAGRAM
 Direction determines the tool:

                 ┌───────────────────┐
  Incoming ────▶ │  Your Controller  │ ────▶ Outgoing
  requests       │  (payments API)   │       calls
                 └───────────────────┘
      │                                      │
      ▼                                      ▼
  WebTestClient                         MockWebServer
  (test YOUR endpoints)                 (fake the DOWNSTREAM)

How does MockWebServer work?

MockWebServer is a real HTTP server from the OkHttp project that binds to an ephemeral local port. You enqueue scripted responses, point your client at the server's URL, and assert the recorded request.

JAVA
package com.bank.client;

import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.web.reactive.function.client.WebClient;

import java.io.IOException;

import static org.junit.jupiter.api.Assertions.*;

class CreditScoreClientTest {

    private MockWebServer server;
    private CreditScoreClient client;

    @BeforeEach
    void setUp() throws IOException {
        server = new MockWebServer();
        server.start();
        String baseUrl = server.url("/").toString();
        client = new CreditScoreClient(WebClient.create(baseUrl));
    }

    @AfterEach
    void tearDown() throws IOException {
        server.shutdown();
    }

    @Test
    void fetchScore_sendsCorrectRequest() throws Exception {
        // Arrange: enqueue a successful response
        server.enqueue(new MockResponse()
            .setBody("{\"accountId\":\"SAV2001\",\"score\":750}")
            .addHeader("Content-Type", "application/json"));

        // Act: call the client
        int score = client.fetchScore("SAV2001");

        // Assert: verify the request our code sent
        RecordedRequest request = server.takeRequest();
        assertEquals("GET", request.getMethod());
        assertEquals("/scores/SAV2001", request.getPath());
        assertEquals(750, score);
    }

    @Test
    void fetchScore_500_throwsDownstreamException() throws Exception {
        // Simulate downstream failure
        server.enqueue(new MockResponse().setResponseCode(500));

        assertThrows(DownstreamException.class, () -> client.fetchScore("SAV2001"));
    }

    @Test
    void fetchScore_404_throwsBureauNotFoundException() throws Exception {
        server.enqueue(new MockResponse().setResponseCode(404));

        assertThrows(BureauNotFoundException.class, () -> client.fetchScore("SAV2001"));
    }

    @Test
    void fetchScore_timeout_throwsTimeoutException() throws Exception {
        // Simulate slow server -- verify our timeout fires
        server.enqueue(new MockResponse()
            .setBody("{\"score\":750}")
            .setBodyDelay(5, java.util.concurrent.TimeUnit.SECONDS));

        // Client has a 2-second timeout configured
        assertThrows(java.util.concurrent.TimeoutException.class,
            () -> client.fetchScore("SAV2001"));
    }
}

How does WebTestClient work?

WebTestClient is an in-memory test client for your own controllers. With @WebFluxTest, Spring loads only the web layer.

JAVA
package com.bank.controller;

import com.bank.model.TransferRequest;
import com.bank.service.TransferService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.bean.MockBean;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;

import java.math.BigDecimal;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@WebFluxTest(TransferController.class)
class TransferControllerTest {

    @Autowired
    private WebTestClient webTestClient;

    @MockBean
    private TransferService transferService;

    @Test
    void transfer_validRequest_returns201() {
        when(transferService.transfer(any()))
            .thenReturn(Mono.just("TXN-12345"));

        TransferRequest request = new TransferRequest(
            "SAV2001", "SAV2002", new BigDecimal("5000.00"));

        webTestClient.post()
            .uri("/api/v1/transfers")
            .bodyValue(request)
            .exchange()
            .expectStatus().isCreated()
            .expectBody()
            .jsonPath("$.transactionId").isEqualTo("TXN-12345")
            .jsonPath("$.status").isEqualTo("COMPLETED");
    }

    @Test
    void transfer_missingAccountId_returns400() {
        TransferRequest request = new TransferRequest(
            null, "SAV2002", new BigDecimal("5000.00"));

        webTestClient.post()
            .uri("/api/v1/transfers")
            .bodyValue(request)
            .exchange()
            .expectStatus().isBadRequest();
    }
}

How do MockWebServer and WebTestClient compare?

| Aspect | MockWebServer | WebTestClient |
|--------|--------------|---------------|
| Tests | Outbound calls (your client code) | Inbound requests (your controller) |
| What it is | Real HTTP server on local port | In-memory test client |
| What it fakes | The downstream service | Nothing (drives your endpoint) |
| Asserts | Request method, path, headers, body | Response status, body, validation |
| Simulates | 500, 404, timeout, slow response | N/A (it is the caller, not the responder) |
| Spring integration | None (standalone) | @WebFluxTest + @MockBean |
| Exercises | Full HTTP client stack (connection, serialization) | Full web layer (routing, validation, serialization) |

How do you combine both in a full slice test?

For a test that spans your controller to a real downstream, use WebTestClient at the front door and MockWebServer at the back.

JAVA
package com.bank.integration;

import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.reactive.server.WebTestClient;

import java.io.IOException;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class FullSliceTest {

    @Autowired
    private WebTestClient webTestClient;

    private MockWebServer creditBureauServer;

    @BeforeEach
    void setUp() throws IOException {
        creditBureauServer = new MockWebServer();
        creditBureauServer.start(8089); // Bureau URL configured to localhost:8089
    }

    @AfterEach
    void tearDown() throws IOException {
        creditBureauServer.shutdown();
    }

    @Test
    void eligibility_bureauReturnsHighScore_approved() {
        // MockWebServer fakes the bureau (outbound)
        creditBureauServer.enqueue(new MockResponse()
            .setBody("{\"score\":800}")
            .addHeader("Content-Type", "application/json"));

        // WebTestClient drives the endpoint (inbound)
        webTestClient.post()
            .uri("/api/v1/eligibility")
            .bodyValue("{\"accountId\":\"SAV2001\"}")
            .exchange()
            .expectStatus().isOk()
            .expectBody()
            .jsonPath("$.decision").isEqualTo("APPROVED");
    }
}

What are common mistakes when choosing the tool?

DIAGRAM
# WRONG: using WebTestClient to test outbound resilience
@WebFluxTest(TransferController.class)
// This tests your CONTROLLER, not your outbound WebClient
// It tells you nothing about retry logic, timeout, or 500 handling

# WRONG: using MockWebServer to test your routing
server.enqueue(new MockResponse().setResponseCode(200));
// This tests the DOWNSTREAM, not your controller's routing or validation

# CORRECT: match the tool to the direction
// Outbound client (calls credit bureau) ──▶ MockWebServer
// Inbound controller (receives transfer request) ──▶ WebTestClient

What are common interview follow-up questions?

  • How do you verify that MockWebServer received the expected request headers and body?
  • Can WebTestClient test a non-reactive Spring MVC controller?
  • How do you simulate network timeouts and connection failures in MockWebServer?
  • When would you use WireMock instead of MockWebServer?

What should a senior engineer know?

The direction of the call decides the tool. MockWebServer exercises the full outbound HTTP client stack: connection, serialization, status handling, timeout, and retry. WebTestClient exercises the full inbound web layer: routing, request deserialization, bean validation, status codes, and response serialization. Using the wrong one tests nothing useful about the seam you care about.

Remember to shut down MockWebServer in teardown and to assert the recorded request, otherwise you only tested that a response came back, not that you sent the right request.

JAVA
// Always assert the recorded request
RecordedRequest request = server.takeRequest();
assertEquals("GET", request.getMethod());
assertEquals("/scores/SAV2001", request.getPath());
assertTrue(request.getHeader("Authorization").startsWith("Bearer "));
// Without this, you only verified the response, not the request your code built

Visual Overview

DIAGRAM
 Tool Selection by Call Direction
 ==================================

                Inbound                    Outbound
                (others call YOU)          (YOU call others)
                     │                         │
                     ▼                         ▼
              ┌──────────────┐          ┌──────────────┐
              │ WebTestClient│          │MockWebServer │
              │              │          │              │
              │ Drives YOUR  │          │ Fakes the    │
              │ controller   │          │ DOWNSTREAM   │
              │              │          │              │
              │ Asserts:     │          │ Asserts:     │
              │ - status     │          │ - request    │
              │ - body       │          │   method     │
              │ - validation │          │ - path       │
              │              │          │ - headers    │
              │ Sliced:      │          │              │
              │ @WebFluxTest │          │ Simulates:   │
              │ + @MockBean  │          │ - 500, 404   │
              │              │          │ - timeout    │
              │              │          │ - retry      │
              └──────────────┘          └──────────────┘

 Full Slice Test (both tools):

 WebTestClient ──▶ Your Controller ──▶ Your Client ──▶ MockWebServer
 (front door)       (real code)         (real code)     (fake bureau)

Key Takeaway

MockWebServer and WebTestClient solve opposite problems. MockWebServer is a real HTTP server you point your outbound client at to test the calls your service makes to downstream services: you enqueue scripted responses, verify the exact request your code sent, and simulate failure like 500s, 404s, and timeouts. WebTestClient drives your own inbound endpoints with @WebFluxTest and mocked collaborators to verify routing, validation, status codes, and response serialization.

The one-line rule is that MockWebServer tests outbound calls and WebTestClient tests inbound calls. The classic mistake is swapping them: using WebTestClient to test outbound resilience tells you nothing about your client, and using MockWebServer to test your routing tells you nothing about your endpoints.

At National Bank of India, our loan-eligibility service calls an external credit bureau that is flaky and costs money per call. We used MockWebServer to simulate bureau failures and verify retry and timeout behavior, and WebTestClient to verify our eligibility endpoint handles validation and returns the correct decisions. For full end-to-end slices we combined both: WebTestClient at the front door and MockWebServer at the back.

Remember one sentence: the direction of the call decides the tool: MockWebServer for outbound to faked downstreams, WebTestClient for inbound to your own controllers.

---

Share
Use ← → arrow keys to navigate