How to mock Spring WebFlux WebClient?
Asked Answered
S

9

78

We wrote a small Spring Boot REST application, which performs a REST request on another REST endpoint.

@RequestMapping("/api/v1")
@SpringBootApplication
@RestController
@Slf4j
public class Application
{
    @Autowired
    private WebClient webClient;

    @RequestMapping(value = "/zyx", method = POST)
    @ResponseBody
    XyzApiResponse zyx(@RequestBody XyzApiRequest request, @RequestHeader HttpHeaders headers)
    {
        webClient.post()
            .uri("/api/v1/someapi")
            .accept(MediaType.APPLICATION_JSON)
            .contentType(MediaType.APPLICATION_JSON)
            .body(BodyInserters.fromObject(request.getData()))
            .exchange()
            .subscribeOn(Schedulers.elastic())
            .flatMap(response ->
                    response.bodyToMono(XyzServiceResponse.class).map(r ->
                    {
                        if (r != null)
                        {
                            r.setStatus(response.statusCode().value());
                        }

                        if (!response.statusCode().is2xxSuccessful())
                        {
                            throw new ProcessResponseException(
                                    "Bad status response code " + response.statusCode() + "!");
                        }

                        return r;
                    }))
            .subscribe(body ->
            {
                // Do various things
            }, throwable ->
            {
                // This section handles request errors
            });

        return XyzApiResponse.OK;
    }
}

We are new to Spring and are having trouble writing a Unit Test for this small code snippet.

Is there an elegant (reactive) way to mock the webClient itself or to start a mock server that the webClient can use as an endpoint?

Stillas answered 25/7, 2017 at 10:53 Comment(1)
To anyone looking to mock webclient, consider putting your webclient codes as a function in a service, because it is way easier to mock a service than the webclientHexameter
V
83

We accomplished this by providing a custom ExchangeFunction that simply returns the response we want to the WebClientBuilder:


webClient = WebClient.builder()
            .exchangeFunction(clientRequest -> 
                    Mono.just(ClientResponse.create(HttpStatus.OK)
                    .header("content-type", "application/json")
                    .body("{ \"key\" : \"value\"}")
                    .build())
            ).build();

myHttpService = new MyHttpService(webClient);

Map<String, String> result = myHttpService.callService().block();

// Do assertions here
    

If we want to use Mokcito to verify if the call was made or reuse the WebClient accross multiple unit tests in the class, we could also mock the exchange function:

@Mock
private ExchangeFunction exchangeFunction;

@BeforeEach
void init() {
    WebClient webClient = WebClient.builder()
            .exchangeFunction(exchangeFunction)
            .build();

    myHttpService = new MyHttpService(webClient);
}

@Test
void callService() {
    when(exchangeFunction.exchange(any(ClientRequest.class)))
   .thenReturn(buildMockResponse());
    Map<String, String> result = myHttpService.callService().block();

    verify(exchangeFunction).exchange(any());

    // Do assertions here
}
    

Note: If you get null pointer exceptions related to publishers on the when call, your IDE might have imported Mono.when instead of Mockito.when.

Sources:

Valedictory answered 1/11, 2019 at 14:0 Comment(5)
I prefer don't use .block() in tests cause it's a bit against the main approach how to write tests in Reactor. Use StepVerifer instead - to wrap you async chain and assert it's results. See here projectreactor.io/docs/test/release/api/reactor/test/…Lollygag
Very valid point @povisenko. Sometimes I just find using .block() when unit testing something trivial is more readable.Valedictory
As mentioned by @homeOfTheWizard this should be the accepted answer.Cuspidate
@Valedictory yeah, agree with you. .block() could be indeed satisfying medium.com/swlh/stepverifier-vs-block-in-reactor-ca754b12846bLollygag
What matcher should I use for a List<CustomObject>? webClient.post().uri(url).body(listOfCustomObjects, CustomObject.class) This is how my webclient call is written. How do I mock body method?Platitudinize
V
33

With the following method it was possible to mock the WebClient with Mockito for calls like this:

webClient
.get()
.uri(url)
.header(headerName, headerValue)
.retrieve()
.bodyToMono(String.class);

or

webClient
.get()
.uri(url)
.headers(hs -> hs.addAll(headers));
.retrieve()
.bodyToMono(String.class);

Mock method:

private static WebClient getWebClientMock(final String resp) {
    final var mock = Mockito.mock(WebClient.class);
    final var uriSpecMock = Mockito.mock(WebClient.RequestHeadersUriSpec.class);
    final var headersSpecMock = Mockito.mock(WebClient.RequestHeadersSpec.class);
    final var responseSpecMock = Mockito.mock(WebClient.ResponseSpec.class);

    when(mock.get()).thenReturn(uriSpecMock);
    when(uriSpecMock.uri(ArgumentMatchers.<String>notNull())).thenReturn(headersSpecMock);
    when(headersSpecMock.header(notNull(), notNull())).thenReturn(headersSpecMock);
    when(headersSpecMock.headers(notNull())).thenReturn(headersSpecMock);
    when(headersSpecMock.retrieve()).thenReturn(responseSpecMock);
    when(responseSpecMock.bodyToMono(ArgumentMatchers.<Class<String>>notNull()))
            .thenReturn(Mono.just(resp));

    return mock;
}
Vaudois answered 18/1, 2019 at 13:10 Comment(4)
seems like a pretty straightforward solution with lack of stubbingLollygag
@IgorsSakels How to use verify with that way?Adiell
from where this notNull() comes from.?? Usually it is good to include those static imports when people answer in SO.Crissie
notNull comes from org.mockito.ArgumentMatchersFimbriate
F
24

You can use MockWebServer by the OkHttp team. Basically, the Spring team uses it for their tests too (at least how they said here). Here is an example with reference to a source:

According to Tim's blog post let's consider that we have the following service:

class ApiCaller {
    
   private WebClient webClient;
    
   ApiCaller(WebClient webClient) {
      this.webClient = webClient;
   }
    
   Mono<SimpleResponseDto> callApi() {
       return webClient.put()
                       .uri("/api/resource")
                       .contentType(MediaType.APPLICATION_JSON)
                       .header("Authorization", "customAuth")
                       .syncBody(new SimpleRequestDto())
                       .retrieve()
                       .bodyToMono(SimpleResponseDto.class);
    }
}

then the test could be designed in the following way (comparing to origin I changed the way how async chains should be tested in Reactor using StepVerifier):

class ApiCallerTest {
  
  private final MockWebServer mockWebServer = new MockWebServer();
  private final ApiCaller apiCaller = new ApiCaller(WebClient.create(mockWebServer.url("/").toString()));
  
  @AfterEach
  void tearDown() throws IOException {
     mockWebServer.shutdown();
  }
  
  @Test
  void call() throws InterruptedException {
       mockWebServer.enqueue(new MockResponse().setResponseCode(200)
                                               .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                                               .setBody("{\"y\": \"value for y\", \"z\": 789}")
      );
      
      //Asserting response
      StepVerifier.create(apiCaller.callApi())
                  .assertNext(res -> {
                        assertNotNull(res);
                        assertEquals("value for y", res.getY());
                        assertEquals("789", res.getZ());
                  })
                  .verifyComplete();
 
     //Asserting request
     RecordedRequest recordedRequest = mockWebServer.takeRequest();
     //use method provided by MockWebServer to assert the request header
     recordedRequest.getHeader("Authorization").equals("customAuth");
     DocumentContext context = >JsonPath.parse(recordedRequest.getBody().inputStream());
     //use JsonPath library to assert the request body
     assertThat(context, isJson(allOf(
            withJsonPath("$.a", is("value1")),
            withJsonPath("$.b", is(123))
     )));
  }
}
Fisken answered 25/3, 2019 at 21:2 Comment(5)
Please note that a link at the bottom of a post does not suffice to give credit when referencing material written by others. Learn more here.Coparcenary
I keep getting the below error when trying this code...anyone able to help? ApiCallerTest.java:19: error: cannot access ExternalResource private final ApiCaller apiCaller = new ApiCaller(WebClient.create(mockWebServer.url("/").toString())); ^ class file for org.junit.rules.ExternalResource not foundConjugation
This is an integration test not a unit test. We are not mocking WebClient in herePerdue
@povisenko That was a rude comment from you. But I am inclusive enough to give my knowledge to you. You are mocking the response using a mock server. You are not completely testing your web client here, you are asserting the response. Try running a mutation test/pit test against your code snippet, it will rip off this test suite. That is the reason I have up voted https://mcmap.net/q/263841/-how-to-mock-spring-webflux-webclient over your answer. And say no to Plagiarism. As Baum said in the first comment of this post, you need to work on reading this stackoverflow.com/help/referencing. Good luck !Perdue
@AkhilGhatiki I've made a referencing more clear, agree that it is relevant comment. I think that you do not mind if I extend your incisiveness with some clarity on integration tests. Common approach for integration tests assumes to reduce amount of mocks to zero. Integration test could use mocks when there are many cross dependencies though, but obviously it is not our case at all. I can advice you to check out this SE topic to softwareengineering.stackexchange.com/q/347323/307798 and check out a blog post by M. Fowler martinfowler.com/bliki/IntegrationTest.html. Cheers!Lollygag
A
9

I use WireMock for integration testing. I think it is much better and supports more functions than OkHttp MockeWebServer. Here is simple example:

public class WireMockTest {

  WireMockServer wireMockServer;
  WebClient webClient;

  @BeforeEach
  void setUp() throws Exception {
    wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
    wireMockServer.start();
    webClient = WebClient.builder().baseUrl(wireMockServer.baseUrl()).build();
  }

  @Test
  void testWireMock() {
    wireMockServer.stubFor(get("/test")
        .willReturn(ok("hello")));

    String body = webClient.get()
        .uri("/test")
        .retrieve()
        .bodyToMono(String.class)
        .block();
    assertEquals("hello", body);
  }

  @AfterEach
  void tearDown() throws Exception {
    wireMockServer.stop();
  }

}

If you really want to mock it I recommend JMockit. There isn't necessary call when many times and you can use the same call like it is in your tested code.

@Test
void testJMockit(@Injectable WebClient webClient) {
  new Expectations() {{
      webClient.get()
          .uri("/test")
          .retrieve()
          .bodyToMono(String.class);
      result = Mono.just("hello");
  }};

  String body = webClient.get()
      .uri(anyString)
      .retrieve()
      .bodyToMono(String.class)
      .block();
  assertEquals("hello", body);
}

Allodial answered 6/5, 2020 at 7:41 Comment(0)
T
5

Wire mocks is suitable for integration tests, while I believe it's not needed for unit tests. While doing unit tests, I will just be interested to know if my WebClient was called with the desired parameters. For that you need a mock of the WebClient instance. Or you could inject a WebClientBuilder instead.

Let's consider the simplified method which does a post request like below.

@Service
@Getter
@Setter
public class RestAdapter {

    public static final String BASE_URI = "http://some/uri";
    public static final String SUB_URI = "some/endpoint";

    @Autowired
    private WebClient.Builder webClientBuilder;

    private WebClient webClient;

    @PostConstruct
    protected void initialize() {
        webClient = webClientBuilder.baseUrl(BASE_URI).build();
    }

    public Mono<String> createSomething(String jsonDetails) {

        return webClient.post()
                .uri(SUB_URI)
                .accept(MediaType.APPLICATION_JSON)
                .body(Mono.just(jsonDetails), String.class)
                .retrieve()
                .bodyToMono(String.class);
    }
}

The method createSomething just accepts a String, assumed as Json for simplicity of the example, does a post request on a URI and returns the output response body which is assumed as a String.

The method can be unit tested as below, with StepVerifier.

public class RestAdapterTest {
    private static final String JSON_INPUT = "{\"name\": \"Test name\"}";
    private static final String TEST_ID = "Test Id";

    private WebClient.Builder webClientBuilder = mock(WebClient.Builder.class);
    private WebClient webClient = mock(WebClient.class);

    private RestAdapter adapter = new RestAdapter();
    private WebClient.RequestBodyUriSpec requestBodyUriSpec = mock(WebClient.RequestBodyUriSpec.class);
    private WebClient.RequestBodySpec requestBodySpec = mock(WebClient.RequestBodySpec.class);
    private WebClient.RequestHeadersSpec requestHeadersSpec = mock(WebClient.RequestHeadersSpec.class);
    private WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class);

    @BeforeEach
    void setup() {
        adapter.setWebClientBuilder(webClientBuilder);
        when(webClientBuilder.baseUrl(anyString())).thenReturn(webClientBuilder);
        when(webClientBuilder.build()).thenReturn(webClient);
        adapter.initialize();
    }

    @Test
    @SuppressWarnings("unchecked")
    void createSomething_withSuccessfulDownstreamResponse_shouldReturnCreatedObjectId() {
        when(webClient.post()).thenReturn(requestBodyUriSpec);
        when(requestBodyUriSpec.uri(RestAdapter.SUB_URI))
                .thenReturn(requestBodySpec);
        when(requestBodySpec.accept(MediaType.APPLICATION_JSON)).thenReturn(requestBodySpec);
        when(requestBodySpec.body(any(Mono.class), eq(String.class)))
                .thenReturn(requestHeadersSpec);
        when(requestHeadersSpec.retrieve()).thenReturn(responseSpec);
        when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.just(TEST_ID));


        ArgumentCaptor<Mono<String>> captor
                = ArgumentCaptor.forClass(Mono.class);

        Mono<String> result = adapter.createSomething(JSON_INPUT);

        verify(requestBodySpec).body(captor.capture(), eq(String.class));
        Mono<String> testBody = captor.getValue();
        assertThat(testBody.block(), equalTo(JSON_INPUT));
        StepVerifier
                .create(result)
                .expectNext(TEST_ID)
                .verifyComplete();
    }
}

Note that the 'when' statements test all the parameters except the request Body. Even if one of the parameters mismatches, the unit test fails, thereby asserting all these. Then, the request body is asserted in a separate verify and assert as the 'Mono' cannot be equated. The result is then verified using step verifier.

And then, we can do an integration test with wire mock, as mentioned in the other answers, to see if this class wires properly, and calls the endpoint with the desired body, etc.

Talus answered 25/7, 2019 at 7:43 Comment(1)
where is the repo code located at? I'm testing it and it doesn't work... it looks like very good but doesn't work, at least for me.Kirovograd
V
5

I have tried all the solutions in the already given answers here. The answer to your question is: It depends if you want to do Unit testing or Integration testing.

For unit testing purpose, mocking the WebClient itself is too verbose and require too much code. Mocking ExchangeFunction is simpler and easier. For this, the accepted answer must be @Renette 's solution.

For integration testing the best is to use OkHttp MockWebServer. Its simple to use an flexible. Using a server allows you to handle some error cases you otherwise need to handle manually in a Unit testing case.

Varela answered 30/4, 2020 at 6:23 Comment(0)
H
1

I wanted to use webclient for unit testing, but mockito was too complex to setup, so i created a library which can be used to build mock webclient in unit tests. This also verifies the url, method, headers and request body before dispatching the response.

    FakeWebClientBuilder fakeWebClientBuilder = FakeWebClientBuilder.useDefaultWebClientBuilder();
    
            FakeRequestResponse fakeRequestResponse = new FakeRequestResponseBuilder()
            .withRequestUrl("https://google.com/foo")
            .withRequestMethod(HttpMethod.POST)
            .withRequestBody(BodyInserters.fromFormData("foo", "bar"))
            .replyWithResponse("test")
            .replyWithResponseStatusCode(200)
            .build();
    
    
    
            WebClient client =
            FakeWebClientBuilder.useDefaultWebClientBuilder()
            .baseUrl("https://google.com")
            .addRequestResponse(fakeRequestResponse)
            .build();
    
            // Our webclient will return `test` when called. 
           // This assertion would check if all our enqueued responses are dequeued by the class or method we intend to test.
           Assertions.assertTrue(fakeWebClientBuilder.assertAllResponsesDispatched());
Hippolytus answered 31/10, 2021 at 11:11 Comment(1)
link your own project correctly. github.com/naveen17797/fakewebclientHexameter
E
1

With spring-cloud-starter-contract-stub-runner you can use Wiremock to mock the API responses. Here you can find a working example I described on medium. The AutoConfigureMockMvc annotation starts a Wiremock server before your test, exposing everything you have in the classpath:/mappings location (probably src/test/resources/mappings on disk).

@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureWireMock(port = 0)
class BalanceServiceTest {
    private static final Logger log = LoggerFactory.getLogger(BalanceServiceTest.class);
    @Autowired
    private BalanceService service;

    @Test
    public void test() throws Exception {

        assertNotNull(service.getBalance("123")
                .get());
    }
}

Here is an example for what a mapping file looks like. The balance.json file contains any json content you need. You can also mimic response delays or failures in static configuration files or programatically. More info on their website.

{
  "request": {
    "method": "GET",
    "url": "/v2/accounts/123/balance"
  },
  "response": {
    "status": 200,
    "delayDistribution": {
      "type": "lognormal",
      "median": 1000,
      "sigma": 0.4
    },
    "headers": {
      "Content-Type": "application/json",
      "Cache-Control": "no-cache"
    },
    "bodyFileName": "balance.json"
  }
}
Exegesis answered 16/11, 2021 at 14:44 Comment(0)
W
0

I highly recommend using Okhttp MockWebServer over mocking. The reason being MockWebServer is a much much cleaner approach.

Below is the code template you can use for unit testing WebClient.

class Test {

  private ClassUnderTest classUnderTest;
  public static MockWebServer mockWebServer;

  @BeforeAll
  static void setUp() throws IOException {
    mockWebServer = new MockWebServer();
    mockWebServer.start();
  }

  @BeforeEach
  void initialize() {
    var httpUrl = mockWebServer.url("/xyz");
    var webClient = WebClient.create(httpUrl.toString());
    classUnderTest = new ClassUnderTest(webClient);
  }

  @Test
  void testMehod() {
    var mockResp = new MockResponse();
    mockResp.setResponseCode(200);
    mockResp.addHeader("Content-Type", "application/json");
    mockResp.setBody(
        "{\"prop\":\"some value\"}");
    mockWebServer.enqueue(mockResp); 
    // This enqueued response will be returned when webclient is invoked
    ...
    ...
    classUnderTest.methodThatInvkesWebClient();
    ...
    ...
  }

  @AfterAll
  static void tearDown() throws IOException {
    mockWebServer.shutdown();
  }
}

Pay special attention to the initialize method. That's the only thing tricky here.

Path /xyz is not the base url, rather your resource path. You don't need to tell the base url to MockWebServer. Reason being, MockWebServer will spin up a server on the local host with some random port. And if you provide your own base url, your unit test will fail.

mockWebServer.url("/xyz")

This will give you base url i.e. the host and port on which MockWebServer is listening plus the resource path, say localhost:8999/xyz. You will need to create WebClient with this url.

WebClient.create(httpUrl.toString())

This will create the WebClient that make calls to the MockWebServer for your unit tests.

Wadewadell answered 19/12, 2022 at 5:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.