Чистый, современный Котлин

Вступление

Kotlin, Spring Boot и WireMock - мощная комбинация для использования при создании службы REST API. Я новичок в Spring Boot, но мне он уже нравится из-за бесшовных API-интерфейсов, которые он предоставляет, например. внедрение зависимостей с использованием @Configuration, @Component и @Bean.

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

В этом руководстве будет рассмотрено следующее:

  • Настройте простой сервис REST API с помощью Kotlin, Spring Boot и Spring WebFlux. Эта служба API будет вызывать API Открытая погода.
  • Напишите модульные тесты с помощью WireMock, чтобы имитировать ответ сервера Open Weather API.

Создать проект загрузки Kotlin Spring

  • Перейдите на https://start.spring.io.
  • Создайте проект Kotlin Gradle и добавьте Spring Reactive Web в качестве зависимости.
  • Наконец, нажмите Создать проект. Будет загружен ZIP-архив со структурой проекта.

Создайте маршрут API и функцию-обработчик

  • Разархивируйте файл и откройте его с помощью IDE. ZIP-файл содержит следующую структуру проекта.

  • Давайте построим наш API-маршрут.

Обратите внимание на аннотацию @Suppress("unused"). Это сделано для того, чтобы избавиться от проверки кода IntelliJ: Маршрут классов никогда не используется, поскольку мы нигде напрямую не используем класс Route. (В этом нет необходимости, кстати, только если вас раздражает то, что проверка кода IntelliJ выделяет неиспользуемый класс.)

Причина этого в том, что Spring Boot фактически управляет регистрацией маршрута через аннотации @Configuration и @Bean.

Spring WebFlux также предлагает маршрут DSL, который вы можете увидеть в функции route. Здесь мы указываем, что наш базовый маршрут API будет иметь префикс /api и принять тип содержимого application/json.

Мы используем nest, который в основном ставит префиксы для любых маршрутов, определенных в блоке; в данном случае это /current-weather.

Итак, если вы хотите добавить больше маршрутов в будущем, вы можете просто поместить их в этот блок. Они автоматически наследуют префикс /api и тип содержимого application/json.

Метод нашего маршрута - POST, а функция обработчика для любого запроса, который попадает в этот маршрут, - getCurrentWeather, предоставляемая классом, реализующим WeatherService. Подробнее об этом позже.

  • Давайте определим наш интерфейс, класс реализации и модели данных.

Интерфейс

Этот интерфейс определяет контракт API и будет реализован реальным классом WeatherService, который передается классу Route выше.

Класс реализации

Аннотация @Component - еще одно «волшебство» Spring, которое мне очень нравится. Он в основном регистрирует этот класс в контексте приложения, чтобы наш класс Route, указанный выше, знал, что он должен использовать этот класс реализации.

Обратите внимание, что класс Route ожидает WeatherService, а OpenWeatherImpl - это класс, реализующий WeatherService.

Давайте продолжим работу над нашим классом реализации, добавив функции для вызова внешнего API.

Spring WebFlux предоставляет WebClient, который мы можем использовать для выполнения HTTP-вызовов. Нам нужно передать это нашему OpenWeatherImpl классу.

Нам нужен другой класс, который будет обрабатывать создание WebClient, необходимых в нашем классе реализации. Цель этого - упростить наш модульный тест, поскольку мы можем имитировать WebClient или настроить его, если захотим.

Обратите внимание на аннотацию @Bean к функции createWebClient. Вот как наша OpenWeatherImpl получит свою зависимость от WebClient объекта.

Кроме того, нам нужно добавить базовый URL-адрес Open Weather API в наш application.properties файл.

Мы передаем это значение нашему классу OpenWeatherConfig выше через аннотацию @Value. В этом руководстве наш ключ API будет храниться как переменная среды OPENWEATHER_API_KEY.

Модель данных

Этот файл определяет модели объекта, который мы отправляем и ожидаем получить от Open Weather API. (Обратите внимание, что мы захватываем только одно поле main, в реальном ответе гораздо больше полей.)

Запустите приложение и вызовите конечную точку API

  • Наша конечная точка API готова к работе, давайте запустим приложение. Перейдите к WiremockdemoApplication.kt и запустите функцию main.
  • Вы можете вызвать эту конечную точку либо с помощью такого инструмента, как Postman, либо просто из командной строки. Вот команда curl, если вы хотите выйти из командной строки.
curl -X POST \
  http://localhost:8080/api/current-weather \
  -H 'Content-Type: application/json' \
  -d '{"cityId": "6619279"}'
# The cityId in this example is for Sydney, Australia, for other cities, please go to openweathermap.org

Вы должны получить такой ответ.

STATUS - 200 OK
Response body
{
  "main": {
    "temp": 284.35,
    "pressure": 1019,
    "humidity": 62,
    "temp_min": 282,
    "temp_max": 287
  }
}

Вуаля! Вы закончили настройку своего маршрута API и его функции обработчика.

Написать модульный тест

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

Добавить WireMock в зависимость проекта

Прежде всего, нам нужно включить пакет WireMock в нашу зависимость проекта, чтобы Gradle мог загрузить и импортировать его в наш проект для нас.

В свой build.gradle.kts добавьте следующий пакет com.github.tomakehurst:wiremock-jre8:2.25.1 в dependencies.

Напомним, ваш build.gradle.kts к настоящему моменту должен выглядеть так.

Как только вы это сделаете, щелкните инструмент Gradle в своем редакторе и нажмите Reimport All Gradle Projects, чтобы убедиться, что пакет WireMock добавлен в проект, чтобы мы могли начать использовать его в нашем тестовом классе. .

Подготовьте фиктивный ответ от Open Weather API

Чтобы смоделировать реальный Open Weather API, нам нужно знать, как выглядит ответ. Итак, давайте сделаем запрос через Postman и сохраним ответ.

Успешный ответ от Open Weather API - это 200 OK тело ответа, которое выглядит следующим образом.

Сохраним это в файле с именем openweather-api-response.json в папке src/test/kotlin/resources. Мы будем использовать этот файл позже, чтобы имитировать ответ API от Open Weather в наших тестовых примерах.

Создайте файл application.properties, чтобы указать локальную конечную точку Open Weather.

Поскольку мы собираемся использовать WireMock сервер для имитации реальной конечной точки Open Weather, нам необходимо настроить наше тестовое приложение так, чтобы оно указывало на localhost вместо реальной конечной точки.

Для этого нам просто нужно создать файл application.properties в папке src/test/kotlin/resources и указать таким образом локальную конечную точку.

Настройте начальную структуру для тестового класса

Переходим к тесту. Сначала создайте новый файл / класс Kotlin в папке src/test/kotlin/io/codebrews/wiremockdemo и назовите его RouteTest.kt.

Мы собираемся аннотировать наш класс RouteTest следующим образом:

  • @SpringBootTest: он создает Spring ApplicationContext и загружает весь контейнер нашего приложения для целей тестирования.
  • @RunWith(SpringRunner.class): Это позволяет нам использовать функции Spring Boot в наших тестах JUnit. Он поддерживает загрузку Spring ApplicationContext и внедрение bean-компонентов через аннотацию @Autowired.

Настройте сервер WebTestClient и WireMock

Компоненты, которые нам нужны для тестирования нашего приложения:

  • WebTestClient: Это HTTP-клиент, предоставляемый Spring для тестирования нашей реактивной конечной точки HTTP. Обратите внимание на аннотацию @Autowired. Это потому, что мы хотим, чтобы Spring внедрил наш WebTestClient, а не инициализировал его самостоятельно. Модификатор доступа private lateinit var для нашего client необходим, иначе @Autowired не сработает.
  • WireMockRule: это компонент, который запускает наш сервер WireMock перед выполнением любых тестовых примеров и выключает сервер после завершения всех тестовых примеров (поищите в Google @ClassRule). Он помещен в companion object и помечен @JvmField, потому что мы хотим, чтобы он вел себя как статический объект Java (как переменная уровня класса). (Конечно, есть другой способ сделать это.)
  • fun stubResponse: Это функция, которая сообщает серверу WireMock, как себя вести, то есть с каким URL-адресом он должен пытаться сопоставить, должен ли он возвращать ответ, и если да, то каким должен быть ответ. Эту функцию следует вызывать в начале тестового примера.

Убедитесь, что ответ Open Weather API загружен

Для этого мы собираемся записывать String представление содержимого openweather-api-response.json файла в консоль и утверждать, что его объект Kotlin не null.

Строка, загружающая содержимое файла в объект String Kotlin, - это private val openWeatherApiResponse: String? = this::class.java.classLoader.getResource(apiResponseFileName)?.readText().

Если вы запустите этот тест, вы увидите, что содержимое openweather-api-response.json регистрируется в консоли и ваш тест выполняется успешно.

Обратите внимание, что есть аннотация, которая добавляется к нашему классу RouteTest, то есть @AutoConfigureWebTestClient. Эта аннотация обеспечивает автоконфигурацию объекта WebTestClient для нашего тестового класса, поэтому мы можем использовать его для запуска наших тестовых примеров позже.

Нам нужна аннотация, потому что мы используем @SpringBootTest для запуска наших тестов. В качестве альтернативы вы можете попробовать использовать аннотацию @WebFluxTest (не рассматривается в этом руководстве).

Просто к сведению, если вы запустите тестовый класс без добавления @AutoConfigureWebTestClient, вы получите следующее исключение:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'io.codebrews.wiremockdemo.RouteTest': Unsatisfied dependency expressed through field 'client'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.test.web.reactive.server.WebTestClient' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

Напишите тестовые примеры для конечных точек API нашего приложения.

Пришло время написать тестовый пример для конечной точки нашего приложения.

Мы сделаем это так, что мы будем имитировать ответ API от реальной конечной точки Open Weather через экземпляр сервера WireMock, который вернет ответ, как если бы это была настоящая конечная точка Open Weather.

Хочу выделить некоторые достоинства фреймворка Spring Boot:

  • Аннотация @SpringBootTest запустит наше приложение Spring Boot для тестирования, которое включает в себя управление контекстом приложения Spring, то есть внедрение зависимостей всех компонентов и bean-компонентов, необходимых для запуска нашего приложения.
  • @AutoConfigureWebTestClient свяжет экземпляр WebTestClient, чтобы он знал, что нужно запустить наше тестовое приложение Spring Boot, то есть настроит порт приложения и привяжет его к WebTestClient. Также обратите внимание, что экземпляр WebTestClient устанавливается Spring через @Autowired.

Мы настраиваем наш сервер WireMock, предоставленный WireMockRule, для возврата ответа JSON, который мы установили как openWeatherApiResponse.

Важно сделать stubResponse(url, openWeatherApiResponse!!) перед отправкой любого HTTP-запроса через наш WebTestClient объект, иначе сервер WireMock не знает, что ответить на наш HTTP-запрос.

Затем это та часть, где мы делаем HTTP-запрос к конечной точке нашего тестового приложения через объект WebTestClient.

client.post()
    .uri("/api/current-weather")
    .body(Mono.just(requestBody), CityId::class.java)
    .exchange()
    .expectStatus().isOk
    .expectBody().json(responseBody)

Обратите внимание, что нам не нужно указывать базовый URL-адрес для .uri(“/api/current-weather”), потому что Spring управляет им через @AutoConfigureWebTestClient.

Наш модульный тест утверждает, что наше приложение вернет статус 200 OK с телом ответа, которое соответствует содержимому в openweather-api-response.json. (Помните, что наше приложение берет только несколько полей из ответа Open Weather API.)

Наконец, наш модульный тест также проверяет URL-адрес, на который наше приложение отправляет HTTP-запрос через verify(getRequestedFor(urlEqualTo(url))).

Обратите внимание, что мы также не указываем базовый путь для этого, и причина в том, что базовый путь взят из нашего application.properties файла. Эта проверка предназначена для того, чтобы убедиться, что наш настоящий WebClient отправляет HTTP-запрос на правильный URL-адрес ресурса.

Идите вперед и запустите тестовый класс. Вы должны увидеть, что оба тестовых примера выполнены успешно. Ура!

Заключительные слова

Если вы зашли так далеко, вы узнаете следующее:

  • Настройка приложения Kotlin Spring Boot с конечной точкой API, которая вызывает внешний API.
  • Использование WebClient, предоставляемого Spring WebFlux, для управления внешними вызовами API.
  • Написание тестового примера для модульного тестирования через WireMock.

Это потрясающе.

Ссылка

Репозиторий GitHub для этого проекта доступен на GitHub.