Unable to save data received by Spring Boot WebCliet - spring-boot

The things I want to do is: to get data from https://jsonplaceholder.typicode.com/ and save those data into my machine. I want to save the posts from this site. I want to do it by Spring Boot WebClient. I have followed several tutorials, articles, and also WebClient documentation. But Unable to save the response in my local database.
The below URL will return one post.
https://jsonplaceholder.typicode.com/posts/1
If I want to return the post as the response of another API it is working fine, but not able to use the inside program. I have tried with WebClient .block(), but it is working for standalone applications but not for web application.
GitLab link of the project
Controller :
#Autowired
private PostService postService;
// working fine.
#GetMapping(value = "posts", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
#ResponseStatus(HttpStatus.OK)
public Flux<Post> findAll() {
return postService.findAll();
}
#GetMapping(value = "postsSave")
#ResponseStatus(HttpStatus.OK)
public String saveAll() {
return postService.saveAll();
}
Service:
#Override
public String saveAll() {
// Post posts = webClient.get()
// .uri("/posts")
// .retrieve()
// .bodyToFlux(Post.class)
// .timeout(Duration.ofMillis(10_000)).blockFirst();
String url = "https://jsonplaceholder.typicode.com/posts/1";
WebClient.Builder builder = WebClient.builder();
Post p = builder.build()
.get()
.uri(url)
.retrieve()
.bodyToMono(Post.class)
.block(); // this line generating error.
postRepository.save(p);
return "saved";
}
Exception StackTrace:
2022-12-07 14:35:44.070 ERROR 6576 --- [ctor-http-nio-3] a.w.r.e.AbstractErrorWebExceptionHandler : [b48b7f19-1] 500 Server Error for HTTP GET "/postsSave"
java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-3
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:83) ~[reactor-core-3.3.9.RELEASE.jar:3.3.9.RELEASE]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ HTTP GET "/postsSave" [ExceptionHandlingWebHandler]
Stack trace:
at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:83) ~[reactor-core-3.3.9.RELEASE.jar:3.3.9.RELEASE]
at reactor.core.publisher.Mono.block(Mono.java:1680) ~[reactor-core-3.3.9.RELEASE.jar:3.3.9.RELEASE]
at com.quantsys.service.PostService.saveAll(PostService.java:53) ~[classes/:na]
at com.quantsys.controller.PostController.saveAll(PostController.java:26) ~[classes/:an]
But the same code of snippet is working within the Bootstrap class:
#SpringBootApplication
public class QuanrsysPostService {
public static void main(String[] args) {
SpringApplication.run(QuanrsysPostService.class, args);
String url = "https://jsonplaceholder.typicode.com/posts/1";
WebClient.Builder builder = WebClient.builder();
Post p = builder.build()
.get()
.uri(url)
.retrieve()
.bodyToMono(Post.class)
.block(); // working here.
System.out.println(p.toString());
}
}

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 reinvoke WebClient's ExchangeFilterFunction on retry

When using reactor's retry(..) operator WebClient exchange filter functions are not triggered on retry. I understand why, but the issue is when a function (like bellow) generates an authentication token with an expiry time. It might happen, while a request is being "retried" the token expires because the Exchange function is not re-invoked during the retry. Is there a way how to re-generate a token for each retry?
Following AuthClientExchangeFunction generates an authentication token (JWT) with an expiration.
public class AuthClientExchangeFunction implements ExchangeFilterFunction {
private final TokenProvider tokenProvider;
public IntraAuthWebClientExchangeFunction(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
#Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
String jwt = tokenProvider.getToken();
return next.exchange(withBearer(request, jwt));
}
private ClientRequest withBearer(ClientRequest request, String jwt){
return ClientRequest.from(request)
.headers(headers -> headers.set(HttpHeaders.AUTHORIZATION, "Bearer "+ jwt))
.build();
}
}
Lets say that a token is valid for 2999 ms -> Each retry request fails due to 401.
WebClient client = WebClient.builder()
.filter(new AuthClientExchangeFunction(tokenProvider))
.build();
client.get()
.uri("/api")
.retrieve()
.bodyToMono(String.class)
.retryBackoff(1, Duration.ofMillis(3000)) ;
Edit
Here is an executable example
#SpringBootTest
#RunWith(SpringRunner.class)
public class RetryApplicationTests {
private static final MockWebServer server = new MockWebServer();
private final RquestCountingFilterFunction requestCounter = new RquestCountingFilterFunction();
#AfterClass
public static void shutdown() throws IOException {
server.shutdown();
}
#Test
public void test() {
server.enqueue(new MockResponse().setResponseCode(500).setBody("{}"));
server.enqueue(new MockResponse().setResponseCode(500).setBody("{}"));
server.enqueue(new MockResponse().setResponseCode(500).setBody("{}"));
server.enqueue(new MockResponse().setResponseCode(200).setBody("{}"));
WebClient webClient = WebClient.builder()
.baseUrl(server.url("/api").toString())
.filter(requestCounter)
.build();
Mono<String> responseMono1 = webClient.get()
.uri("/api")
.retrieve()
.bodyToMono(String.class)
.retryBackoff(3, Duration.ofMillis(1000)) ;
StepVerifier.create(responseMono1).expectNextCount(1).verifyComplete();
assertThat(requestCounter.count()).isEqualTo(4);
}
static class RquestCountingFilterFunction implements ExchangeFilterFunction {
final Logger log = LoggerFactory.getLogger(getClass());
final AtomicInteger counter = new AtomicInteger();
#Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
log.info("Sending {} request to {} {}", counter.incrementAndGet(), request.method(), request.url());
return next.exchange(request);
}
int count() {
return counter.get();
}
}
}
output
MockWebServer[44855] starting to accept connections
Sending 1 request to GET http://localhost:44855/api/api
MockWebServer[44855] received request: GET /api/api HTTP/1.1 and responded: HTTP/1.1 500 Server Error
MockWebServer[44855] received request: GET /api/api HTTP/1.1 and responded: HTTP/1.1 500 Server Error
MockWebServer[44855] received request: GET /api/api HTTP/1.1 and responded: HTTP/1.1 500 Server Error
MockWebServer[44855] received request: GET /api/api HTTP/1.1 and responded: HTTP/1.1 200 OK
org.junit.ComparisonFailure:
Expected :4
Actual :1
You need to update your spring-boot version to 2.2.0.RELEASE. retry() will not invoke exchange function in previous version.
I've tested this using a simple code (In Kotlin).
#Component
class AnswerPub {
val webClient = WebClient.builder()
.filter(PrintExchangeFunction())
.baseUrl("https://jsonplaceholder.typicode.com").build()
fun productInfo(): Mono<User> {
return webClient
.get()
.uri("/todos2/1")
.retrieve()
.bodyToMono(User::class.java)
.retry(2) { it is Exception }
}
data class User(
val id: String,
val userId: String,
val title: String,
val completed: Boolean
)
}
class PrintExchangeFunction : ExchangeFilterFunction {
override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
println("Filtered")
return next.exchange(request)
}
}
And the console output looked like:
2019-10-29 09:31:55.912 INFO 12206 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
2019-10-29 09:31:55.917 INFO 12206 --- [ main] c.e.s.SpringWfDemoApplicationKt : Started SpringWfDemoApplicationKt in 3.19 seconds (JVM running for 4.234)
Filtered
Filtered
Filtered
So in my case, the exchange function is invoked every single time.

Testing a Post multipart/form-data request on REST Controller

I've written a typical spring boot application, now I want to add integration tests to that application.
I've got the following controller and test:
Controller:
#RestController
public class PictureController {
#RequestMapping(value = "/uploadpicture", method = RequestMethod.POST)
public ResponseEntity<VehicleRegistrationData> uploadPicturePost(#RequestPart("userId") String userId, #RequestPart("file") MultipartFile file) {
try {
return ResponseEntity.ok(sPicture.saveAndParsePicture(userId, file));
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
}
Test:
#Test
public void authorizedGetRequest() throws Exception {
File data = ResourceUtils.getFile(testImageResource);
byte[] bytes = FileUtils.readFileToByteArray(data);
ObjectMapper objectMapper = new ObjectMapper();
MockMultipartFile file = new MockMultipartFile("file", "test.jpg", MediaType.IMAGE_JPEG_VALUE, bytes);
MockMultipartFile userId =
new MockMultipartFile("userId",
"userId",
MediaType.MULTIPART_FORM_DATA_VALUE,
objectMapper.writeValueAsString("123456").getBytes()
);
this.mockMvc.perform(multipart("/uploadPicture")
.file(userId)
.file(file)
.header(API_KEY_HEADER, API_KEY)).andExpect(status().isOk());
}
Testing the controller with the OkHttp3 client on android works seamlessly, but I can't figure out how to make that request work on the MockMvc
I expect 200 as a status code, but get 404 since, I guess, the format is not the correct one for that controller
What am I doing wrong?
It must be a typo.
In your controller, you claim the request URL to be /uploadpicture, but you visit /uploadPicture for unit test.

Zuul proxy server throwing Internal Server Error when request takes more time to process

Zuul Proxy Error
I am getting this error when the request takes more time to process in the service.But Zuul returns response of Internal Server Error
Using zuul 2.0.0.RC2 release
As far as I understand, in case of a service not responding, a missing route, etc. you can setup the /error endpoint to deliver a custom response to the user.
For example:
#Controller
public class CustomErrorController implements ErrorController {
#RequestMapping(value = "/error", produces = "application/json")
public #ResponseBody
ResponseEntity error(HttpServletRequest request) {
// consider putting these in a try catch
Integer statusCode = (Integer)request.getAttribute("javax.servlet.error.status_code");
Throwable exception = (Throwable)request.getAttribute("javax.servlet.error.exception");
// maybe add some error logging here, e.g. original status code, exception, traceid, etc.
// consider a better error to the user here
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("{'message':'some error happened', 'trace_id':'some-trace-id-here'}");
}
#Override
public String getErrorPath() {
return "/error";
}
}

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