How can I unit test the exchange()
method of Spring's new RestClient with code coverage?
I have a method which fires an http post request to another third party service:
public ResponseEntity<String> upload(Path imagePath, Path metadataPath) {
MultiValueMap<String, Object> form = new LinkedMultiValueMap<>();
form.add("photo", new FileSystemResource(imagePath));
form.add("metadata", new FileSystemResource(metadataPath));
return restClientBuilderProvider
.fotoUploadClientBuilder()
.build()
.post()
.uri(uriBuilder -> uriBuilder.path("/upload").build())
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(form)
.exchange((clientRequest, clientResponse) -> {
InputStream body = clientResponse.getBody();
String bodyString = new String(body.readAllBytes());
if (clientResponse.getStatusCode().isError()) {
log.error(UPLOAD_FAILED_ERROR_MESSAGE, imagePath, metadataPath, bodyString);
}
return ResponseEntity.status(clientResponse.getStatusCode()).body(bodyString);
});
}
Below is the method fotoUploadClientBuilder
:
public RestClient.Builder fotoUploadClientBuilder() {
return RestClient
.builder()
.baseUrl(fotoUploadBaseUrl)
.defaultHeaders(headers -> headers.setBasicAuth(fotoUploadUsername, fotoUploadPassword))
.requestFactory(fotoUploadClientBuilderFactory);
}
Below is the unit test:
@Test
@SuppressWarnings({"unchecked"})
void uploadFilesNoSuccessfulUploadsInProtocol() {
//this first block is not really relevant for the question, its just preparation for the method invocation
String year = "2023";
String month = "05";
String day = "04";
FileUpload fileUpload = new FileUpload();
FileUploadProtocol fileUploadProtocol = new FileUploadProtocol();
fileUpload.setFileUploadProtocol(fileUploadProtocol);
fileUploadProtocol.setFileUploads(List.of(fileUpload));
Path imagePath = Paths.get("123.jpg");
Path metaPath = Paths.get("123.xml");
Map<Path, Path> uploadFiles = Map.of(imagePath, metaPath);
Path imagePath = Paths.get("123.jpg");
Path metaPath = Paths.get("123.xml");
Map<Path, Path> uploadFiles = Map.of(imagePath, metaPath);
RestClient.Builder mockedRestClientBuilder = Mockito.mock(RestClient.Builder.class);
RestClient mockedRestClient = Mockito.mock(RestClient.class);
RestClient.RequestBodyUriSpec requestBodyUriSpec = Mockito.mock(RestClient.RequestBodyUriSpec.class);
Mockito.when(restClientBuilderProvider.fotoUploadClientBuilder()).thenReturn(mockedRestClientBuilder);
Mockito.when(mockedRestClientBuilder.build()).thenReturn(mockedRestClient);
Mockito.when(mockedRestClient.post()).thenReturn(requestBodyUriSpec);
RestClient.RequestBodySpec requestBodySpec = Mockito.mock(RestClient.RequestBodySpec.class);
ResponseEntity<String> responseEntity = Mockito.mock(ResponseEntity.class);
Mockito.when(requestBodyUriSpec.uri(any(Function.class))).thenReturn(requestBodyUriSpec);
Mockito.when(requestBodyUriSpec.contentType(MediaType.MULTIPART_FORM_DATA)).thenReturn(requestBodyUriSpec);
Mockito.when(requestBodyUriSpec.body(any(Object.class))).thenReturn(requestBodySpec);
Mockito.when(requestBodySpec.exchange(Mockito.any())).thenReturn(responseEntity);
Mockito.when(responseEntity.getStatusCode()).thenReturn(HttpStatus.BAD_REQUEST);
Mockito.when(
fileUploadRepositoryMock.findFileUploadByImagePathIsAndStatusIsNotAndYearIsAndMonthIsAndDayIs(
imagePath.toString(),
HttpStatus.NO_CONTENT.value(),
year,
month,
day
)
).thenReturn(fileUpload);
// this method calls the `upload` method I am trying to test with coverage
uploadService.uploadFiles(year, month, day, uploadFiles);
Mockito.verify(fileUploadRepositoryMock).save(fileUpload);
Mockito.verify(fileUploadProtocolRepositoryMock).save(fileUploadProtocol);
Assertions.assertThat(fileUploadProtocol.getAmountSuccessfulUploads()).isZero();
This unit test works fine. What I am missing is the coverage of the call of the exchange
method in the chain, see image below:
I am aware of mocking the whole set up for the http POST as I don't want to set up a mock webserver since I'm not testing an api I provide. I consume an api of a third party service and therefore mock the whole request.
My question is how can I access clientRequest
and clientResponse
parameters of this lambda expression to verify that clientResponse
will give me the BAD_REQUEST
status or maybe mock both params to make the response return the status I want to?
Mockito.when(requestBodySpec.exchange(Mockito.any()))
.thenReturn(responseEntity);
stubs the method to always return the same responseEntity
and completely ignores the argument passed to the function. Consequently, the code in the lambda expression is never executed (because nobody is calling it, you stubbed your method to do nothing with the argument).
Lambdas are not part of the regular, "linear" control flow. They defer execution of a block of code, until explicitly invoked. Here's a simpler example without Mockito mock objects:
System.out.println("before lambda");
final Supplier<String> lambda = () -> {
System.out.println("inside lambda");
return "lambda return value";
};
System.out.println("after lambda");
If you run above code, you will only see "before lambda" and "after lambda" in your output. If you collect code coverage metrics, you will find that the lambda's code is not covered, because the lambda is not invoked (only defined). Only when you call lambda.get()
, the lambda gets invoked and its body executed; you will then see the "inside lambda" message in your output and coverage for the lines.
If you want coverage of the lines, you must execute the lines. You could do this with the .thenAnswer
method when stubbing to manually invoke the lambda:
when(requestBodySpec.exchange(any()))
.thenAnswer(a -> {
final RestClient.RequestHeadersSpec.ExchangeFunction<String> lambda -> a.getArgument(0);
return lambda.exchange(yourClientRequest, yourClientResponse); // invoke the lambda
});
clientRequest
is not used in your lambda, so you could pass null
instead.
But I doubt the usefulness of such tests. When you mock 99-100% of your method, what are you really testing? The method looks like it should be tested with an integration test instead. You can spin up a local test server to send send the request to, or you could use functionality provided by the Spring Framework to make testing rest client interactions easier.