Как я могу протестировать метод exchange()
нового RestClient Spring с покрытием кода?
У меня есть метод, который отправляет HTTP-запрос в другую стороннюю службу:
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);
});
}
Ниже приведен метод fotoUploadClientBuilder
:
public RestClient.Builder fotoUploadClientBuilder() {
return RestClient
.builder()
.baseUrl(fotoUploadBaseUrl)
.defaultHeaders(headers -> headers.setBasicAuth(fotoUploadUsername, fotoUploadPassword))
.requestFactory(fotoUploadClientBuilderFactory);
}
Ниже приведен модульный тест:
@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();
Этот модульный тест работает нормально. Чего мне не хватает, так это покрытия вызова метода exchange
в цепочке, см. изображение ниже:
Я знаю, что высмеивает всю настройку HTTP POST, поскольку я не хочу настраивать макет веб-сервера, поскольку я не тестирую предоставляемый API. Я использую API стороннего сервиса и поэтому высмеиваю весь запрос.
Мой вопрос: как я могу получить доступ к параметрам clientRequest
и clientResponse
этого лямбда-выражения, чтобы убедиться, что clientResponse
даст мне статус BAD_REQUEST
, или, может быть, высмеивать оба параметра, чтобы ответ возвращал тот статус, который я хочу?
@knittl Я не понимаю. Предоставленный мной код соответствует строке 88 на снимке экрана, что приводит меня к выводу, что он выполняет имитируемый запрос.
Код делает только то, что вы ему говорите. Вы заглушили свой макет Mockito, чтобы он возвращал статические значения. Вы никогда не добавляли заглушку в свой макет Mockito для вызова лямбды, а определили свою заглушку как thenReturn(responseEntity)
. Заглушая метод, вы игнорируете реальную реализацию метода и вместо этого ничего не делаете. Настоящий метод exchange
вызывает лямбду, а ваши заглушенные методы — нет.
Я даже не знаю, как и где fileUploadRepositoryMock
играет роль. Он заглушен в вашем тесте, но не вызывается вашей реализацией.
@knittl В данном случае это не особо нужно. fileUploadRepositoryMock
участвует в приведенном выше методе, который вызывает upload method
Mockito.when(requestBodySpec.exchange(Mockito.any()))
.thenReturn(responseEntity);
заглушает метод, чтобы всегда возвращать одно и то же responseEntity
, и полностью игнорирует аргумент, переданный функции. Следовательно, код в лямбда-выражении никогда не выполняется (поскольку его никто не вызывает, вы заглушили свой метод, чтобы ничего не делать с аргументом).
Лямбды не являются частью обычного, «линейного» потока управления. Они откладывают выполнение блока кода до тех пор, пока он не будет вызван явно. Вот более простой пример без фиктивных объектов Mockito:
System.out.println("before lambda");
final Supplier<String> lambda = () -> {
System.out.println("inside lambda");
return "lambda return value";
};
System.out.println("after lambda");
Если вы запустите приведенный выше код, в выводе вы увидите только «до лямбды» и «после лямбды». Если вы соберете метрики покрытия кода, вы обнаружите, что лямбда-код не охвачен, поскольку лямбда-выражение не вызывается (только определяется). Только когда вы вызываете lambda.get()
, вызывается лямбда и выполняется ее тело; после этого вы увидите сообщение «внутренняя лямбда» в своем выводе и покрытие строк.
Если вы хотите охватить линии, вы должны их выполнить. Вы можете сделать это с помощью метода .thenAnswer
при заглушке для ручного вызова лямбды:
when(requestBodySpec.exchange(any()))
.thenAnswer(a -> {
final RestClient.RequestHeadersSpec.ExchangeFunction<String> lambda -> a.getArgument(0);
return lambda.exchange(yourClientRequest, yourClientResponse); // invoke the lambda
});
clientRequest
не используется в вашей лямбде, поэтому вместо этого вы можете передать null
.
Но я сомневаюсь в полезности таких тестов. Когда вы высмеиваете 99–100% своего метода, что вы на самом деле тестируете? Похоже, что вместо этого метод следует протестировать с помощью интеграционного теста. Вы можете развернуть локальный тестовый сервер для отправки запроса или использовать функции, предоставляемые Spring Framework, чтобы упростить тестирование взаимодействия с остальными клиентами.
как я могу получить доступ к параметру yourClientResponse
? Mockito ArgumentCaptor
вызывает исключение, и я думаю, что не могу/не должен его создавать. Я просто хочу получить доступ к уже существующему.
@ArthurEirich, такого не существует. Вы заменяете RestClient тестовой двойной реализацией. Вы несете ответственность за поведение тестового двойника. Настоящий экземпляр клиента отдыха знает запрос и ответ и передает его лямбде при его вызове. Вы заменили реальную реализацию, поэтому вам придется позаботиться о предоставлении запроса и ответа.
Ах хорошо. После издевательства над HttpRequest и HttpResponse и манипулирования ответом, чтобы вернуть то, что мне нужно, я наконец получил то, что хотел. Спасибо, что разъяснили и научили :)
Ваш поставщик клиента отдыха и ваш клиент отдыха оба издеваются, поэтому они не выполняют ваш реальный код.