Модульное тестирование Новый метод обмена RestClient Spring() с покрытием кода

Как я могу протестировать метод 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 21.06.2024 10:29

@knittl Я не понимаю. Предоставленный мной код соответствует строке 88 на снимке экрана, что приводит меня к выводу, что он выполняет имитируемый запрос.

Arthur Eirich 21.06.2024 10:40

Код делает только то, что вы ему говорите. Вы заглушили свой макет Mockito, чтобы он возвращал статические значения. Вы никогда не добавляли заглушку в свой макет Mockito для вызова лямбды, а определили свою заглушку как thenReturn(responseEntity). Заглушая метод, вы игнорируете реальную реализацию метода и вместо этого ничего не делаете. Настоящий метод exchange вызывает лямбду, а ваши заглушенные методы — нет.

knittl 21.06.2024 10:46

Я даже не знаю, как и где fileUploadRepositoryMock играет роль. Он заглушен в вашем тесте, но не вызывается вашей реализацией.

knittl 21.06.2024 10:58

@knittl В данном случае это не особо нужно. fileUploadRepositoryMock участвует в приведенном выше методе, который вызывает upload method

Arthur Eirich 21.06.2024 11:00
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
5
68
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий
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 вызывает исключение, и я думаю, что не могу/не должен его создавать. Я просто хочу получить доступ к уже существующему.

Arthur Eirich 21.06.2024 11:01

@ArthurEirich, такого не существует. Вы заменяете RestClient тестовой двойной реализацией. Вы несете ответственность за поведение тестового двойника. Настоящий экземпляр клиента отдыха знает запрос и ответ и передает его лямбде при его вызове. Вы заменили реальную реализацию, поэтому вам придется позаботиться о предоставлении запроса и ответа.

knittl 21.06.2024 11:03

Ах хорошо. После издевательства над HttpRequest и HttpResponse и манипулирования ответом, чтобы вернуть то, что мне нужно, я наконец получил то, что хотел. Спасибо, что разъяснили и научили :)

Arthur Eirich 21.06.2024 11:15

Другие вопросы по теме