paint-brush
Создание эффективных интеграционных тестов: лучшие практики и инструменты в Spring Frameworkк@avvero
531 чтения
531 чтения

Создание эффективных интеграционных тестов: лучшие практики и инструменты в Spring Framework

к Anton Belyaev8m2024/05/26
Read on Terminal Reader

Слишком долго; Читать

В этой статье предлагаются практические рекомендации по написанию интеграционных тестов, демонстрирующие, как сосредоточиться на спецификациях взаимодействия с внешними сервисами, сделав тесты более читабельными и простыми в сопровождении. Такой подход не только повышает эффективность тестирования, но и способствует лучшему пониманию процессов интеграции внутри приложения. Через призму конкретных примеров будут рассмотрены различные стратегии и инструменты, такие как оболочки DSL, JsonAssert и Pact, предлагающие читателю исчерпывающее руководство по повышению качества и наглядности интеграционных тестов.
featured image - Создание эффективных интеграционных тестов: лучшие практики и инструменты в Spring Framework
Anton Belyaev HackerNoon profile picture
0-item

В современной разработке программного обеспечения ключевую роль в обеспечении надежности и стабильности приложений играет эффективное тестирование.


В этой статье предлагаются практические рекомендации по написанию интеграционных тестов, демонстрирующие, как сосредоточиться на спецификациях взаимодействия с внешними сервисами, сделав тесты более читабельными и простыми в сопровождении. Такой подход не только повышает эффективность тестирования, но и способствует лучшему пониманию процессов интеграции внутри приложения. Через призму конкретных примеров будут рассмотрены различные стратегии и инструменты, такие как оболочки DSL, JsonAssert и Pact, предлагающие читателю исчерпывающее руководство по повышению качества и наглядности интеграционных тестов.


В статье представлены примеры интеграционных тестов, выполненных с использованием Spock Framework в Groovy для проверки HTTP-взаимодействий в приложениях Spring. В то же время предложенные основные техники и подходы могут эффективно применяться к различным типам взаимодействий, выходящим за рамки HTTP.

описание проблемы

В статье «Написание эффективных интеграционных тестов в Spring: стратегии организованного тестирования для мокинга HTTP-запросов» описан подход к написанию тестов с четким разделением на отдельные этапы, каждый из которых выполняет свою конкретную роль. Опишем тестовый пример по этим рекомендациям, но с мокингом не одного, а двух запросов. Стадия Act (Execution) для краткости будет опущена (полный тестовый пример можно найти в репозитории проекта ).

Представленный код условно разделен на части: «Поддерживающий код» (выделен серым цветом) и «Спецификация внешних взаимодействий» (выделен синим цветом). Вспомогательный код включает механизмы и утилиты для тестирования, включая перехват запросов и эмуляцию ответов. Спецификация внешних взаимодействий описывает конкретные данные о внешних сервисах, с которыми система должна взаимодействовать во время теста, включая ожидаемые запросы и ответы. Вспомогательный код закладывает основу для тестирования, а Спецификация напрямую связана с бизнес-логикой и основными функциями системы, которую мы пытаемся протестировать.


Спецификация занимает незначительную часть кода, но представляет значительную ценность для понимания теста, тогда как вспомогательный код, занимающий большую часть, представляет меньшую ценность и повторяется для каждого макетного объявления. Код предназначен для использования с MockRestServiceServer. Обращаясь к примеру на WireMock , можно увидеть ту же картину: спецификация практически идентична, а Supporting Code различается.


Цель этой статьи — предложить практические рекомендации по написанию тестов таким образом, чтобы основное внимание уделялось спецификации, а вспомогательный код отходил на второй план.

Демонстрационный сценарий

Для нашего тестового сценария я предлагаю гипотетического бота Telegram, который пересылает запросы к OpenAI API и отправляет ответы обратно пользователям.

Контракты взаимодействия с сервисами описаны в упрощенном виде, чтобы выделить основную логику работы. Ниже приведена диаграмма последовательности, демонстрирующая архитектуру приложения. Я понимаю, что дизайн может вызвать вопросы с точки зрения архитектуры системы, но, пожалуйста, подойдите к этому с пониманием — основная цель здесь — продемонстрировать подход к повышению наглядности в тестах.

Предложение

В данной статье рассматриваются следующие практические рекомендации по написанию тестов:

  • Использование оберток DSL для работы с макетами.
  • Использование JsonAssert для проверки результатов.
  • Хранение спецификаций внешних взаимодействий в файлах JSON.
  • Использование файлов Pact.

Использование оболочек DSL для насмешек

Использование оболочки DSL позволяет скрыть шаблонный макет кода и предоставляет простой интерфейс для работы со спецификацией. Важно подчеркнуть, что предлагается не конкретный DSL, а общий подход, который он реализует. Исправленный пример теста с использованием DSL представлен ниже ( полный текст теста ).

 setup: def openaiRequestCaptor = restExpectation.openai.completions(withSuccess("{...}")) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 1

Где метод restExpectation.openai.completions , например, описывается следующим образом:

 public interface OpenaiMock { /** * This method configures the mock request to the following URL: {@code https://api.openai.com/v1/chat/completions} */ RequestCaptor completions(DefaultResponseCreator responseCreator); }

Наличие комментария к методу позволяет при наведении курсора на имя метода в редакторе кода получить помощь, в том числе увидеть URL-адрес, который будет осмеян.

В предлагаемой реализации объявление ответа из макета производится с использованием экземпляров ResponseCreator , что позволяет создавать собственные, такие как:

 public static ResponseCreator withResourceAccessException() { return (request) -> { throw new ResourceAccessException("Error"); }; }

Ниже показан пример теста для неудачных сценариев с указанием набора ответов:

 import static org.springframework.http.HttpStatus.FORBIDDEN setup: def openaiRequestCaptor = restExpectation.openai.completions(openaiResponse) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 0 where: openaiResponse | _ withResourceAccessException() | _ withStatus(FORBIDDEN) | _

Для WireMock всё то же самое, за исключением того, что немного отличается формирование ответа ( тестовый код , код фабричного класса ответа ).

Использование аннотации @Language("JSON") для лучшей интеграции IDE

При реализации DSL можно аннотировать параметры метода с помощью @Language("JSON") чтобы включить поддержку языковых функций для определенных фрагментов кода в IntelliJ IDEA. Например, при использовании JSON редактор будет обрабатывать строковый параметр как код JSON, обеспечивая такие функции, как подсветка синтаксиса, автозаполнение, проверка ошибок, навигация и поиск по структуре. Вот пример использования аннотации:

 public static DefaultResponseCreator withSuccess(@Language("JSON") String body) { return MockRestResponseCreators.withSuccess(body, APPLICATION_JSON); }

Вот как это выглядит в редакторе:

Использование JsonAssert для проверки результатов

Библиотека JSONAssert предназначена для упрощения тестирования структур JSON. Он позволяет разработчикам легко сравнивать ожидаемые и фактические строки JSON с высокой степенью гибкости, поддерживая различные режимы сравнения.

Это позволяет перейти от такого описания проверки

 openaiRequestCaptor.body.model == "gpt-3.5-turbo" openaiRequestCaptor.body.messages.size() == 1 openaiRequestCaptor.body.messages[0].role == "user" openaiRequestCaptor.body.messages[0].content == "Hello!"

что-то вроде этого

 assertEquals("""{ "model": "gpt-3.5-turbo", "messages": [{ "role": "user", "content": "Hello!" }] }""", openaiRequestCaptor.bodyString, false)

На мой взгляд, главное преимущество второго подхода в том, что он обеспечивает согласованность представления данных в различных контекстах — в документации, журналах и тестах. Это значительно упрощает процесс тестирования, обеспечивая гибкость сравнения и точность диагностики ошибок. Таким образом, мы не только экономим время на написании и сопровождении тестов, но и улучшаем их читабельность и информативность.

При работе в Spring Boot, начиная как минимум с версии 2, для работы с библиотекой не нужны дополнительные зависимости, поскольку org.springframework.boot:spring-boot-starter-test уже включает зависимость от org.skyscreamer:jsonassert .

Сохранение спецификации внешних взаимодействий в файлах JSON

Мы можем сделать одно наблюдение: строки JSON занимают значительную часть теста. Должны ли они быть скрыты? Да и нет. Важно понять, что приносит больше пользы. Их скрытие делает тесты более компактными и упрощает понимание сути теста с первого взгляда. С другой стороны, при тщательном анализе часть важной информации о спецификации внешнего взаимодействия будет скрыта, что потребует дополнительных переходов по файлам. Решение зависит от удобства: делайте так, как вам удобнее.

Если вы решите хранить строки JSON в файлах, один из простых вариантов — хранить ответы и запросы отдельно в файлах JSON. Ниже приведен тестовый код ( полная версия ), демонстрирующий вариант реализации:

 setup: def openaiRequestCaptor = restExpectation.openai.completions(withSuccess(fromFile("json/openai/response.json"))) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 1

Метод fromFile просто считывает строку из файла в каталоге src/test/resources и не несет в себе какой-либо революционной идеи, но по-прежнему доступен в репозитории проекта для справки.

Для переменной части строки предлагается использовать замену на org.apache.commons.text.StringSubstitutor и передавать набор значений при описании макета, например:

 setup: def openaiRequestCaptor = restExpectation.openai.completions(withSuccess(fromFile("json/openai/response.json", [content: "Hello! How can I assist you today?"])))

Где часть с заменой в файле JSON выглядит так:

 ... "message": { "role": "assistant", "content": "${content:-Hello there, how may I assist you today?}" }, ...

Единственная задача для разработчиков при внедрении подхода к хранению файлов — разработать правильную схему размещения файлов в тестовых ресурсах и схему именования. Легко допустить ошибку, которая может ухудшить качество работы с этими файлами. Одним из решений этой проблемы может быть использование спецификаций, например, из Pact, которые будут обсуждаться позже.

При использовании описанного подхода в тестах, написанных на Groovy, можно столкнуться с неудобством: в IntelliJ IDEA нет поддержки перехода к файлу из кода, но в будущем ожидается добавление поддержки этого функционала . В тестах, написанных на Java, это прекрасно работает.

Использование файлов контрактов Pact

Начнем с терминологии.


Контрактное тестирование — это метод тестирования точек интеграции, при котором каждое приложение тестируется изолированно, чтобы подтвердить, что сообщения, которые оно отправляет или получает, соответствуют взаимопониманию, задокументированному в «контракте». Такой подход гарантирует, что взаимодействие между различными частями системы соответствует ожиданиям.


Контракт в контексте контрактного тестирования — это документ или спецификация, в которой зафиксировано соглашение о формате и структуре сообщений (запросов и ответов), которыми обмениваются приложения. Он служит основой для проверки того, что каждое приложение может правильно обрабатывать данные, отправленные и полученные другими участниками интеграции.


Контракт устанавливается между потребителем (например, клиентом, желающим получить некоторые данные) и поставщиком (например, API на сервере, предоставляющим данные, необходимые клиенту).


Тестирование, управляемое потребителями, — это подход к контрактному тестированию, при котором потребители создают контракты во время автоматизированных тестовых запусков. Эти контракты передаются поставщику, который затем запускает набор автоматических тестов. Каждый запрос, содержащийся в файле контракта, отправляется провайдеру, а полученный ответ сравнивается с ожидаемым ответом, указанным в файле контракта. Если оба ответа совпадают, это означает, что потребитель и поставщик услуг совместимы.


Наконец, Пакт. Pact — это инструмент, реализующий идеи тестирования контрактов, ориентированного на потребителя. Он поддерживает тестирование как HTTP-интеграций, так и интеграций на основе сообщений, уделяя особое внимание разработке тестов с упором на код.

Как я упоминал ранее, для нашей задачи мы можем использовать спецификации и инструменты контрактов Pact. Реализация может выглядеть так ( полный тестовый код ):

 setup: def openaiRequestCaptor = restExpectation.openai.completions(fromContract("openai/SuccessfulCompletion-Hello.json")) def telegramRequestCaptor = restExpectation.telegram.sendMessage(withSuccess("{}")) when: ... then: openaiRequestCaptor.times == 1 telegramRequestCaptor.times == 1

Файл контракта доступен для просмотра .

Преимущество использования файлов контрактов заключается в том, что они содержат не только тело запроса и ответа, но и другие элементы спецификации внешних взаимодействий — путь запроса, заголовки и статус ответа HTTP, что позволяет полностью описать макет на основе такого контракта.

Важно отметить, что в этом случае мы ограничиваемся контрактным тестированием и не распространяемся на тестирование, ориентированное на потребителя. Однако кто-то может захотеть изучить Pact дальше.

Заключение

В этой статье были рассмотрены практические рекомендации по повышению наглядности и эффективности интеграционных тестов в контексте разработки с помощью Spring Framework. Моя цель состояла в том, чтобы сосредоточиться на важности четкого определения спецификаций внешних взаимодействий и минимизации шаблонного кода. Для достижения этой цели я предложил использовать DSL-обертки и JsonAssert, хранить спецификации в файлах JSON и работать с контрактами через Pact. Описанные в статье подходы направлены на упрощение процесса написания и сопровождения тестов, улучшение их читаемости и, самое главное, повышение качества самого тестирования за счет точного отражения взаимодействий между компонентами системы.


Ссылка на репозиторий проекта, демонстрирующий тесты — sandbox/bot .


Спасибо за внимание к статье и удачи в написании эффективных и наглядных тестов!