WebClient Integration Test with MockWebServer - spring

I dont know if my configuration is wrong or something with mockwebserver.
I have method in one of my services
public UserResponse getUsername(String username) {
return webClient.get()
.uri("http://user-service/api/user",
uriBuilder -> uriBuilder.queryParam("username", username).build())
.retrieve()
.bodyToMono(UserResponse.class)
.block();
}
And I want to test this method by MockWebServer as below
public class CardWebClientTest {
private MockWebServer server;
private CardService service;
#BeforeEach
void setup() {
server = new MockWebServer();
WebClient webClient = WebClient.builder().baseUrl(server.url("").toString()).build();
service = new CardService(webClient);
}
#Test
void test() {
server.enqueue(new MockResponse().setBody("{\"id\": 12345}")
.addHeader("Content-Type", "application/json")
.setResponseCode(200));
var result = service.getUsername("john");
assertThat(result.getId()).isEqualTo(12345L);
}
Am I missing something? It's propably something trivial but I can't find solution.
CardService which is calling UserService are communicating in EurekaServer with loadbalancer but Im assuming MockWebServer can handle this without any further configuration or not - idk. I will appreciate any help.
UserResponse dto looks like this
public class UserResponse
private Long id;
I forgot to mention error message. It's always
Failed to resolve 'user-service' after 2 queries ; nested exception is java.net.UnknownHostException: Failed to resolve 'user-service' after 2 queries
Alright. Now I can see that mockwebserver has different hostname - it's calling http://kubernetes.docker.internal:52654/api/user and webclient is asking for http://user-service. But still I dont know how to configure mockwebserver to get response. I should set somehow hostname or maybe leave like that and just test webclient.get() to see if response is correct?

Related

How to do integration tests for endpoints that use ZeroCopyHttpOutputMessage

I have an endpoint that casts the org.springframework.http.server.reactive.ServerHttpResponse to org.springframework.http.ZeroCopyHttpOutputMessage.
#SneakyThrows
#GetMapping("/document/{documentId}")
public Mono<Void> serveDocument(#PathVariable final String documentId, final ServerHttpResponse response) {
final Path documentLocation = fileManipulatorService.newFile(stagingConfigurationProperties.location(), documentId);
return ((ZeroCopyHttpOutputMessage) response)
.writeWith(documentLocation, 0, fileManipulatorService.size(documentLocation))
.then(deleteIfExists(documentLocation));
}
Usually, this works well but when calling the endpoint with org.springframework.test.web.reactive.server.WebTestClient the call fails with the following exception:
2022-12-30T18:49:07.678+01:00 ERROR 1392 --- [ parallel-1] a.w.r.e.AbstractErrorWebExceptionHandler : [1848ca22] 500 Server Error for HTTP GET "/document/11c92bad-6fe4-4c85-9d54-4bf4bbad3581"
java.lang.ClassCastException: class org.springframework.mock.http.server.reactive.MockServerHttpResponse cannot be cast to class org.springframework.http.ZeroCopyHttpOutputMessage (org.springframework.mock.http.server.reactive.MockServerHttpResponse and org.springframework.http.ZeroCopyHttpOutputMessage are in unnamed module of loader 'app')
at com.github.bottomlessarchive.loa.stage.view.document.controller.StageDocumentController.serveDocument(StageDocumentController.java:53) ~[main/:na]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
*__checkpoint ? HTTP GET "/document/11c92bad-6fe4-4c85-9d54-4bf4bbad3581" [ExceptionHandlingWebHandler]
This is what my test looks like:
#Test
void testServeDocument() {
final UUID documentId = UUID.randomUUID();
final byte[] content = {1, 2, 3, 4};
final Path contentPath = setupFakeFile("/stage/" + documentId, content);
when(fileManipulatorService.newFile("/stage/", documentId.toString()))
.thenReturn(contentPath);
final byte[] responseBody = webTestClient.get()
.uri("/document/" + documentId)
.exchange()
.expectStatus()
.isOk()
.expectBody()
.returnResult()
.getResponseBody();
assertThat(responseBody)
.isEqualTo(content);
assertThat(contentPath)
.doesNotExist();
}
For me, everything seems to be right. Is there a reason why MockServerHttpResponse doesn't extend ZeroCopyHttpOutputMessage? I wanted to file a bug report to Spring Boot because of this, but before doing so, I came to the conclusion that it might be a better idea to ask first on Stackoverflow.
Firstly, MockServerHttpResponse is a general use mock implementation of a response for tests without an actual server, so it's implemented in a way that is just sufficient and convenient for testing.
Secondly, it doesn't look like any guarantees were ever given that a response in a ServerWebExchange must implement ZeroCopyHttpOutputMessage so I wouldn't blindly cast it without prior type checking.
Another caveat, on Netty even if a response is a ZeroCopyHttpOutputMessage, the transfer will use zero-byte copy only when the specified path resolves to a local file system File, and compression and SSL/TLS are not enabled. Otherwise chunked read/write will be used.
( https://projectreactor.io/docs/netty/release/api/reactor/netty/NettyOutbound.html#sendFile-java.nio.file.Path-long-long- ).
Considering all this I'd refactor your controller to something like this:
#SneakyThrows
#GetMapping("/document/{documentId}")
public Mono<Void> serveDocument(#PathVariable final String documentId, final ServerHttpResponse response) {
...
if (response instanceof ZeroCopyHttpOutputMessage zeroCopyHttpOutputMessage) {
return zeroCopyHttpOutputMessage
.writeWith(documentLocation, 0, ...)
...
}
return response
.writeWith(DataBufferUtils.read(documentLocation, response.bufferFactory(), bufferSize))
...
}
To test the ZeroCopyHttpOutputMessage part of this controller in your integration tests you can use a real (non-mocked) web environment and bind your WebTestClient to that like so:
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTests {
#LocalServerPort
private Integer serverPort;
...
#Test
void testServeDocument() {
WebTestClient webTestClient = WebTestClient
.bindToServer()
.baseUrl("http://localhost:" + serverPort)
.build();
...

How to replace a resttemplate implementation with webclient

I'm trying to replace a resttemplate implementation with a webclient one. The tricky stuff here is that I need to modify a property from an input object, when the response resolves. I don't find the way to achieve it...
This is the resttemplate code:
public Instance login(final Instance instancia, final LoginDTO dto) {
String url = instancia.getBalancer() + API_AUTHENTICATE_PATH;
HttpEntity<LoginDTO> request = generateRequest(dto);
ResponseEntity<JWTToken> token = restTemplate.postForEntity(url, request, JWTToken.class);
instancia.setToken(token.getBody().getIdToken());
return instancia;
}
And this is what I have until now:
#Override
public Mono<Instance> login(Instance instancia, LoginDTO dto) {
Mono<JWTToken> monoToken=webClient.post().uri(url).body((BodyInserters.fromObject(dto))).retrieve()
.bodyToMono(JWTToken.class);
return {....};
}
I'm stucked in that part, because I don't find the way to alter the Instance object...
And there is another point: This is injected in another class, because I need to run this request in parallel against multiple targets. So, a block call is not enough.
Does someone have an idea about how to do it?
Thanks a lot in advance!
It can be achieved easily as following:
#Override
public Mono<Instance> login(Instance instancia, LoginDTO dto) {
return webClient
.post()
.uri(url)
.body((BodyInserters.fromObject(dto)))
.retrieve()
.bodyToMono(JWTToken.class)
.map(token -> {
instancia.setToken(token.getBody().getIdToken());
return instancia;
});
}

Trying to mock restClient external API but it is invoking the actual API in java

I am trying to mock restClient external API but it is invoking the actual API instead of mocking it.
Kindly help as I am not sure where I am going wrong.
I tried mocking the call and a few more other things but it didn't work.
public class TestService
{
private static final String EXTERNAL_API = "http://LinktoExternalAPI/";
#Autowired
RestTemplate restTemplate;
public Map<String, String> countryCodes()
{
Map<String, String> map = new TreeMap<>();
try
{
ResponseEntity<testCountry[]> responseEntity = restTemplate
.getForEntity(EXTERNAL_API
, testCountry[].class);
List<testCountry> testCountryList = Arrays.asList(responseEntity.getBody());
map = testCountryList.stream()
.collect(Collectors.toMap(testCountry::getCode, testCountry::getName));
}
catch (HttpClientErrorException | HttpServerErrorException httpClientOrServerExc)
{
}
return map;
}
}
Test case for this is below:
#RunWith(PowerMockRunner.class)
public class TestServiceTest
{
#InjectMocks
TestService testService;
#Mock
RestTemplate restTemplate;
private static final String EXTERNAL_API = "http://LinktoExternalAPI/";
#Test
public void testCountryCodes(){
TestCountry testCountry = new TestCountry();
testCountry.setCode("JPN");
testCountry.setName("Japan");
List<testCountry> testCountryList = new ArrayList<testCountry>();
testCountryList.add(testCountry);
Mockito.when(restTemplate.getForEntity(EXTERNAL_API, testCountry[].class)).thenReturn(new ResponseEntity(testCountryList, HttpStatus.OK));
Map<String, String> result = testService.countryCodes();
// result is pulling the actual size of the api instead of mocking and sending me testCountryList size.
<Will mention assertion here>
}
The result is pulling the actual size of the API instead of mocking and sending me testCountryList size.
The reason behind the actual API being called is probably that the URL you are mocking is not exactly the same as that being generated at runtime, because of which a mock is not found and actual API is called.
In these cases, you can use Mockito.any().
So the mock code will be Mockito.when(restTemplate.getForEntity(Mockito.any(), Mockito.any())).thenReturn(new ResponseEntity(testCountryList, HttpStatus.OK));
#RunWith(MockitoJUnitRunner.class)
public class TestServiceTest {
#InjectMocks
private TestService testService;
#Mock
private RestTemplate restTemplate;
#Test
public void testCountryCodes(){
TestCountry testCountry = new TestCountry();
testCountry.setCode("JPN");
testCountry.setName("Japan");
TestCountry[] testCountryList = {
testCountry
};
Mockito.when(restTemplate.getForEntity(Mockito.anyString(), Mockito.any())).thenReturn(new ResponseEntity(testCountryList, HttpStatus.OK));
Map<String, String> result = testService.countryCodes();
// result is pulling the actual size of the API instead of mocking and sending me testCountryList size.
}
}
Also try using #RunWith(MockitoJUnitRunner.class) instead of PowerMockRunner.class since you don't seem to be needing the PowerMock capabilities.
You are mocking the wrong method definition.
A getForObject method with the parameters String and Class does not exist. You need to define behaviour for this method.
Note that in your case the third parameter (the varargs) is not used, so it defaults to an empty array. However Mockito requires this information to mock the correct call.
Mockito.when(restTemplate.getForObject(any(String.class), any(Class.class), ArgumentMatchers.<Object>any()))
.thenReturn(result);
For a more complete example check my answer here.

SpringBoot: LoadBalancer [server]: Error choosing server for key default

I'm creating a load balance feature on my project in which I have three server that will simultaneously ping for 15 seconds. However, when I already run my client-side, it always goes to the fallback page and received an error of "LoadBalancer [server]: Error choosing server for key default" even if the servers are already running.
Here are the codes in my project:
app.properties
server.port=8788
server.ribbon.eureka.enabled=false
server.ribbon.listOfServers=localhost:8787,localhost:8789,localhost:8790
#every 15 seconds
server.ribbon.ServerListRefreshInterval=15000
client service (wherein it is my fallback method)
private LoadBalancerClient loadBalancer;
private RestTemplate restTemplate;
public ClientService(RestTemplate rest) {
this.restTemplate = rest;
}
#HystrixCommand(fallbackMethod = "reliable")
public String login() {
ServiceInstance instance = loadBalancer.choose("server");
URI uri = URI.create(String.format("http://%s:%s/admin/ping", instance.getHost(), instance.getPort()));
//URI uri = URI.create("http://localhost:8787/admin/ping");
return this.restTemplate.getForObject(uri, String.class);
}
MainController
public class MainController{
private final static Logger LOGGER = LoggerFactory.getLogger(MainController.class);
#Autowired
private ClientService clientService;
#LoadBalanced
#Bean
public RestTemplate rest(RestTemplateBuilder builder) {
return builder.build();
}
#Autowired
RestTemplate restTemplate;
...
Client client = new Client();
WebResource resource = client.resource("http://%s:%s/auth/loginvalidate");
ClientResponse response = resource.type(MediaType.APPLICATION_JSON)
.header("Authorization", "Basic " + encodePw)
.get(ClientResponse.class);
I got rid of that error by doing two things:
1) Add the following properties to the remote service:
management.endpoints.web.exposure.include: "*"
management.endpoint.health.enabled: "true"
management.endpoint.restart.enabled: "true"
management.endpoint.info.enabled: "true"
2) Make sure that there is a ping endpoint in the remote service:
public class MainController{
#RequestMapping("/")
public String ribbonPing() {
return this.hostName;
}
}
I added a few amendments to the example provided by Kubernetes Circuit Breaker & Load Balancer Example to test this scenario and put in here.
I suggest that you follow those links as a kind of "best practises" guide in order to build your Hystrix/Ribbon solution. Pay special attention to:
the starters/dependencies added to the pom files
the structure of the Java classes (how and where each bean is declared and injected)
how you configure your (micro-)services (in this case with K8s ConfigMaps)

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