Чистый, современный Котлин
Вступление
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
: он создает SpringApplicationContext
и загружает весь контейнер нашего приложения для целей тестирования.@RunWith(SpringRunner.class)
: Это позволяет нам использовать функции Spring Boot в наших тестах JUnit. Он поддерживает загрузку SpringApplicationContext
и внедрение 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.