Spring Boot WebFlux with RouterFunction log request and response body - spring-boot

I'm trying to log request and response body of every call received by my microservice which is using reactive WebFlux with routing function defined as bellow:
#Configuration
public class FluxRouter {
#Autowired
LoggingHandlerFilterFunction loggingHandlerFilterFunction;
#Bean
public RouterFunction<ServerResponse> routes(FluxHandler fluxHandler) {
return RouterFunctions
.route(POST("/post-flux").and(accept(MediaType.APPLICATION_JSON)), fluxHandler::testFlux)
.filter(loggingHandlerFilterFunction);
}
}
#Component
public class FluxHandler {
private static final Logger logger = LoggerFactory.getLogger(FluxHandler.class);
#LogExecutionTime(ActionType.APP_LOGIC)
public Mono<ServerResponse> testFlux(ServerRequest request) {
logger.info("FluxHandler.testFlux");
return request
.bodyToMono(TestBody.class)
.doOnNext(body -> logger.info("FluxHandler-2: "+ body.getName()))
.flatMap(testBody -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue("Hello, ")));
}
}
and a router filter defined like this:
#Component
public class LoggingHandlerFilterFunction implements HandlerFilterFunction<ServerResponse, ServerResponse> {
private static final Logger logger = LoggerFactory.getLogger(LoggingHandlerFilterFunction.class);
#Override
public Mono<ServerResponse> filter(ServerRequest request, HandlerFunction<ServerResponse> handlerFunction) {
logger.info("Inside LoggingHandlerFilterFunction");
return request.bodyToMono(Object.class)
.doOnNext(o -> logger.info("Request body: "+ o.toString()))
.flatMap(testBody -> handlerFunction.handle(request))
.doOnNext(serverResponse -> logger.info("Response body: "+ serverResponse.toString()));
}
}
Console logs here are:
Inside LoggingHandlerFilterFunction
LoggingHandlerFilterFunction-1: {name=Name, surname=Surname}
FluxHandler.testFlux
As you can see I'm able to log the request but after doing it the body inside the FluxHandler seems empty because this is not triggered .doOnNext(body -> logger.info("FluxHandler-2: "+ body.getName()) and also no response body is produced. Can someone show me how to fix this or is there any other way to log request/response body when using routes in webflux?

Related

Spring Webflux: Read request-body in ServerWebExchangeMatcher

I have to create an application with just one endpoint where users can log in and do their other operational stuff. Users should be able to log in with a request body like:
{
"login": {
"token": "12345"
}
}
So every request with a body like this should be allowed via .permitAll() by the WebFilterChain.
The chain is configured like this:
#Bean
public SecurityWebFilterChain chain(ServerHttpSecurity http) {
return http.authorizeExchange(exchanges -> {
exchanges.matchers(new LoginRequestMatcher())
.permitAll()
.pathMatchers("api/v1*")
.hasRole("USER");
})
.csrf()
.disable()
.build();
}
The LoginRequestMatcher analyzes the body of the request and returns a Match if the pattern is ok. So far, this works quite fine.
My problem is that later in the Controller, the request body can not be accessed any more, so I'm currently trying to put it into a cache or the context to be able to access it later on.
Here is my current implementation of the LoginRequestMatcher:
public class LoginRequestMatcher implements ServerWebExchangeMatcher {
private static final Pattern loginRequestPattern = Pattern.compile("...loginPattern");
#Override
public Mono<MatchResult> matches(final ServerWebExchange exchange) {
return exchange.getRequest().getBody()
.map(dataBuffer -> {
byte[] byteArray=new byte[dataBuffer.readableByteCount()];
dataBuffer.read(byteArray);
DataBufferUtils.release(dataBuffer);
return byteArray;
}).defaultIfEmpty(new byte[0])
.map(bytes -> {
ServerHttpRequestDecorator decorator = initDecorator(exchange, bytes);
exchange.mutate().request(decorator).build();
Mono.just(decorator).contextWrite(ctx -> ctx.put("decorator",decorator))
return decorator;
}
)
// Rest of the code checks for Regex in the body and works fine.
.next();
Now, when I try to get the cached request-body after the RequestMatcher is processed (f.e. in a WebFilter), the context does not contain an object with the key "decorator".
#Configuration
#ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public class HttpRequestBodyCachingFilter implements WebFilter {
#Override
public Mono<Void> filter(final ServerWebExchange exchange, final WebFilterChain chain) {
final HttpMethod method = exchange.getRequest().getMethod();
// Nothing to cache for GET and DELETE
if (method==null || HttpMethod.GET==method || HttpMethod.DELETE==method) {
return chain.filter(exchange);
}
// Get decorator from context
return Mono.just(exchange)
//ctx.get("decorator") throws Exception here, because the key does not exist.
.flatMap(s -> Mono.deferContextual(ctx -> ctx.get("decorator")))
.map(decorator ->
(ServerHttpRequestDecorator) decorator)
.flatMap(decorator -> chain.filter(exchange.mutate().request(decorator).build()));
}
}
How can I make the body readable after accessing it in inside a ServerWebExchangeMatcher?

spring boot web client and writing pact contracts

So im trying to figure out how to write consumer contracts for the following class. I have written junit tests fine using mockwebserver.
However for pact testing im struggling and cant seem to see how you get the weblient to use the response from server, all the examples tend to be for resttemplate.
public class OrdersGateway {
public static final String PATH = "/orders";
private final WebClient webClient;
#Autowired
public OrdersGateway(String baseURL) {
this.webClient = WebClient.builder()
.baseUrl(baseURL)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.ALL_VALUE)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
#Override
public Orderresponse findOrders() {
return this.webClient
.post()
.uri(PATH)
.httpRequest(httpRequest -> {
HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
reactorRequest.responseTimeout(Duration.ofSeconds(4));
})
.exchangeToMono(response())
.block();
}
private Function<ClientResponse, Mono<OrderResponse>> response() {
return result -> {
if (result.statusCode().equals(HttpStatus.OK)) {
return result.bodyToMono(OrderResponse.class);
} else {
String exception = String.format("error", result.statusCode());
return Mono.error(new IllegalStateException(exception));
}
};
}
}
Its the #test method for verification, im not sure how to create that. I cant see how the pact-mock-server can intercept the webcleint call.
There might be an assumption that Pact automatically intercepts requests - this is not the case.
So when you write a Pact unit test, you need to explicitly configure your API client to communicate to the Pact mock service, not the real thing.
Using this example as a basis, your test might look like this:
#ExtendWith(PactConsumerTestExt.class)
#PactTestFor(providerName = "orders-gateway")
public class OrdersPactTest {
#Pact(consumer="orders-provider")
public RequestResponsePact findOrders(PactDslWithProvider builder) {
PactDslJsonBody body = PactDslJsonArray.arrayEachLike()
.uuid("id", "5cc989d0-d800-434c-b4bb-b1268499e850")
.stringType("status", "STATUS")
.decimalType("amount", 100.0)
.closeObject();
return builder
.given("orders exist")
.uponReceiving("a request to find orders")
.path("/orders")
.method("GET")
.willRespondWith()
.status(200)
.body(body)
.toPact();
}
#PactTestFor(pactMethod = "findOrders")
#Test
public void findOrders(MockServer mockServer) throws IOException {
OrdersGateway orders = new OrdersGateway(mockServer.getUrl()).findOrders();
// do some assertions
}
}

Webflux: Microservice A POST to Microservice B

I guess my answer is already on this forum (because I read), but I can not understand my concern ...
I want microservice A to send a POST to microservice B.
microserviceA:
webClientBuilder.build().post().uri("http://localhost:9644/relever/add")
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(releve), Web.class)
.retrieve()
.bodyToMono(Web.class);
microservice B (controller):
#RestController
#RequestMapping("/relever")
public class ReleverController {
#PostMapping("/add")
#ResponseStatus(HttpStatus.CREATED)
public void AjouteDunRelever(Relever relever) {
System.out.println("ok!!!");
}
}
When I use POSTMAN in POST mode to http://localhost:9644/relever/add, microservice B reacts, but when microservice A post to microservice B, microservice B does not react, ... why?
thank you!
I think you need to set the baseUrl before building the client.
Try
webClientBuilder.baseUrl("http://localhost:9644").build().post().uri("/relever/add")
.contentType(MediaType.APPLICATION_JSON)
.body(Mono.just(releve), Web.class)
.retrieve()
.bodyToMono(Web.class);
WebClient.create(baseUrl) is basically just doing that for you.
From: org.springframework.web.reactive.function.client.WebClient
* Variant of {#link #create()} that accepts a default base URL. For more
* details see {#link Builder#baseUrl(String) Builder.baseUrl(String)}.
* #param baseUrl the base URI for all requests
* #see #builder()
*/
static WebClient create(String baseUrl) {
return new DefaultWebClientBuilder().baseUrl(baseUrl).build();
}
I changed the baseurl to localhost and with the microservice name, no reaction!
webClientBuilder.baseUrl("http://localhost:9644").build().post().uri("/relever/add")
.body(Mono.just(releve), Web.class)
.accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> {
System.out.println(response.statusCode());
System.out.println(); **<---- Breakpoint Here**
return response.bodyToMono(Web.class);
});
at the top of my class I do this:
#Autowired
private WebClient.Builder webClientBuilder;
and in another package I load the loadbalancer
#Configuration
public class Config {
private static final org.apache.logging.log4j.Logger logger
= LogManager.getLogger(Config.class.getName());
#Bean
#LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
}
The breakpoint doesn't even stop (so it never goes into flatmap), and no reaction in the microserviceB controller
Thank you

How to handle exceptions thrown by the webclient?

I'm trying to figure out how to log exceptions from the webclient, whatever the error status code that is returned from the api that gets called.
I've seen the following implementation:
.onStatus(status -> status.value() != HttpStatus.OK.value(),
rs -> rs.bodyToMono(String.class).map(body -> new IOException(String.format(
"Response HTTP code is different from 200: %s, body: '%s'", rs.statusCode(), body))))
Another example I've seen uses a filter. I guess this filter could be used to log errors as well, aside from requests like in this example:
public MyClient(WebClient.Builder webClientBuilder) {
webClient = webClientBuilder // you can also just use WebClient.builder()
.baseUrl("https://httpbin.org")
.filter(logRequest()) // here is the magic
.build();
}
But are we serious that there is no dedicated exception handler to this thing?
Found it.
bodyToMono throws a WebClientException if the status code is 4xx (client error) or 5xx (Server error).
Full implementation of the service:
#Service
public class FacebookService {
private static final Logger LOG = LoggerFactory.getLogger(FacebookService.class);
private static final String URL_DEBUG = "https://graph.facebook.com/debug_token";
private WebClient webClient;
public FacebookService() {
webClient = WebClient.builder()
.filter(logRequest())
.build();
}
public Mono<DebugTokenResponse> verifyFbAccessToken(String fbAccessToken, String fbAppToken) {
LOG.info("verifyFacebookToken for " + String.format("fbAccessToken: %s and fbAppToken: %s", fbAccessToken, fbAppToken));
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(URL_DEBUG)
.queryParam("input_token", fbAccessToken)
.queryParam("access_token", fbAppToken);
return this.webClient.get()
.uri(builder.toUriString())
.retrieve()
.bodyToMono(DebugTokenResponse.class);
}
private static ExchangeFilterFunction logRequest() {
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
LOG.info("Request: {} {}", clientRequest.method(), clientRequest.url());
clientRequest.headers().forEach((name, values) -> values.forEach(value -> LOG.info("{}={}", name, value)));
return Mono.just(clientRequest);
});
}
#ExceptionHandler(WebClientResponseException.class)
public ResponseEntity<String> handleWebClientResponseException(WebClientResponseException ex) {
LOG.error("Error from WebClient - Status {}, Body {}", ex.getRawStatusCode(), ex.getResponseBodyAsString(), ex);
return ResponseEntity.status(ex.getRawStatusCode()).body(ex.getResponseBodyAsString());
}
}

How to mock Spring WebFlux WebClient?

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?
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:
WebClient
javadoc
WebClient.Builder
javadoc
ExchangeFunction
javadoc
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;
}
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))
)));
}
}
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);
}
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.
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.
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"
}
}
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());
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.

Resources