Contract First или страх и ненависть в королевстве CodeGen
SQA Days 34
Аннотация
Чтобы end-2-end тесты были максимально полезными, их нужно начинать писать параллельно с разработкой самой задачи. Но как это сделать прозрачно, ведь API может поменяться как во время разработки, так и в процессе доработок по другим задачам. Рассмотрим подход Contract First на базе OpenAPI как средство поддержания актуальности e2e тестов.
План
- Как быстро начать автоматизацию задачи и следить за изменениями в автоматизированном режиме?
- Что такое контракт? OpenAPI.
- Рассматриваем что умеет проект OpenAPI Generator
- Берем OpenAPI Generator и по контракту генерируем модели и клиента
RestAssured
. - Убираем лишние файлы.
- Используем свои шаблоны генерации кода по OpenAPI.
- А что делать, если реализация еще недоступна? С помощью Postman и OpenAPI создаем Mock Server и делаем вызовы.
- Вместо выводов: как максимально быстро реагировать в тестах на изменения в коде?
Подготовка
Для выполнения мастер-класса нужно:
Код проекта: openapi-generation.
$ git clone git@github.com:Romanow/openapi-generation.git
$ ./gradlew clean build
Сервис servers
развернут по адресу https://servers.romanow-alex.ru](https://servers.romanow-alex.ru) (на время
доклада). Либо можно запустить локально:
$ docker compose up -d --wait
Импульс
Аннотация
Часто для решения одной бизнес задачи требуется коммуникация и совместная разработка нескольких команд. В микросервисной архитектуре задача упрощается, ведь сервисы имеют разную кодовую базу и взаимодействуют друг с другом по API. А значит требуется лишь договориться об общем API в начале разработки. Но так ли все просто и как автоматизировать этот процесс рассмотрим в нашем докладе.
План доклада
- Коммуникация между командами разработки при совместной работе над одной задачей.
- Что такое контракт? OpenAPI.
- Contract First vs. Code First. Плюсы и минусы подходов.
- Рассматриваем что умеет проект OpenAPI Generator.
- Генерируем код по контракту на клиенте и сервере, разбираем что получилось.
- Убираем лишние файлы.
- Используем свои шаблоны генерации кода по OpenAPI.
- Вместо выводов: как поддержать баланс между чувством прекрасного и сгенерированным кодом?
Доклад
Коммуникация между командами разработки при совместной работе над одной задачей
В мире микросервисов практически любая задача требует взаимодействия нескольких сервисов, а значит для ее выполнения требуется параллельно вести разработку в нескольких командах. Для этого нужна договоренность между командами, по какому API (Application Programming Interface) ону будут взаимодействовать. Ведь если Сервис А вызывает Сервис B, то команда, ответственная за Сервис A не может ждать, пока команда, ответственная за Сервис B возьмет задачу в работу и что-то напишет. Следовательно, нам надо заранее (до начала разработки) описать некоторый контракт, который будет реализовываться в Сервисе B, а Сервис А может начать его использовать на заглушках (например, с помощью WireMock или Postman Mock Server).
Как контролировать корректность данных, которые будут в заглушках, мы не будем рассматривать, скажу лишь, что стоит смотреть в сторону контрактных тестов (Использование Spring Cloud Contract как альтернатива для интеграционных тестов).
Как быстро начать автоматизацию задачи и следить за изменениями в автоматизированном режиме?
Когда вашу систему разрабатывают несколько команд, сложно следить за актуальностью моделей и API (кто-то что-то поменял и забыл вас известить об этом). Хотелось бы отлавливать ситуацию, что что-то поменялось, как можно раньше, например на этапе компиляции.
Так же, когда появляется новый сервис, для старта автоматизации приходится писать много boilerplate кода.
Что такое контракт? OpenAPI
Контракт – это соглашение о том, как будет выглядеть наше API и какие параметры оно будет принимать.
В рамках доклада будем рассматривать синхронную коммуникацию с помощью REST, следовательно, мы строим наше API на базе протокола HTTP. Самым распространенным способом описания контракта для REST сервисов является OpenAPI.
Спецификация OpenAPI определяет стандарт независимого от языка описания API, который позволяет людям и машинам понимать возможности службы без доступа к исходному коду, документации или путем перехвата сетевого трафика. По сути, OpenAPI — это описание методов API, для которых описываются заголовки, входные и выходные параметры.
Contract First vs. Code First. Плюсы и минусы подхода
Итак, мы описали и согласовали контракт между командами, теперь можем приступать к реализации.
Но как поддерживать консистентность контракта с тем, что реально реализовано в коде? Можно при каждом релизе сравнивать
код с контрактом, но все равно в каких-то мелочах они могут разойтись, а случае ошибки может быть сложно установить ее
причину. Например: метод ищет сущность по ID (/api/v1/users/1
), в ответ может вернуться 404 Not Found
, если сущность
не найдена. Но 404 Not Found
может вернуться и в случае, если такого метода больше нет в коде. И для разбора в этой
проблеме потребуется время.
Единственный надежный способ поддерживать в консистентном состоянии два источника правды – это получать одно из другого. Рассмотрим два подхода:
- Code First – мы сначала пишем код, помечаем методы специальными аннотациями
@Tag
,@Opearation
и т.п., а по этим аннотациям автоматически генерируется документация. В итоге получаем OpenAPI, полностью соответствующий коду. Но в этом подходе есть 3 проблемы:- в коде не все можно описать (доп. параметры, валидаторы и т.п.), следовательно, мы можем получить невалидный или неполный OpenAPI;
- требуется время для написания кода, следовательно, другие команды вынуждены нас ждать;
- есть проблемы при генерации методов со сходной сигнатурой (
/api/v1/users
и/api/v1/users?login={1}
) – они сливаются в один метод.
- Contract First – у нас есть согласованный со всеми сторонами контакт, по нему мы генерируем код. Сгенерированный код рабочий, но проблема в том, что он выглядит очень плохо и работать с ним зачастую неудобно. Плюс в команде есть Code Style, соглашения о названиях классов и т.п., а значит нам хочется, чтобы весь код в проекте удовлетворял этим критериям.
Рассматриваем что умеет проект OpenAPI Generator
Погрузимся глубже в Contract First и посмотрим что можно улучшить. Для генерации кода возьмем самое распространенное решение – проект OpenAPI Generator и посмотрим что оно умеет:
- генерация клиентского и серверного кода для всем популярных языков и фреймворков (Java, Kotlin, Go, C#, JavaScript и т.п.);
- валидация OpenAPI;
- возможность кастомизации шаблонов.
# установка OpenAPI Generator
$ npm install @openapitools/openapi-generator-cli -g
Генерируем код по контракту на клиенте и сервере, разбираем что получилось
Рассматривать будем пример OpenAPI servers.yml, сгенерируем клиентский код с помощью openapi-generator:
$ openapi-generator generate \
-g kotlin \
--api-package ru.romanow.openapi.client.rest \
--model-package ru.romanow.openapi.client.models \
--additional-properties=dateLibrary=java8,serializationLibrary=jackson,enumPropertyNaming=UPPERCASE \
-o client/build/generated \
-i openapi/servers.yml
В результате получаем целый проект:
Посмотрим на файл ServerResponse:
/**
* Please note:
* This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* Do not edit this file manually.
*/
@file:Suppress("ArrayInDataClass", "EnumEntryName", "RemoveRedundantQualifierName", "UnusedImport")
package ru.romanow.openapi.client.models
import ru.romanow.openapi.client.models.StateInfo
import com.fasterxml.jackson.annotation.JsonProperty
data class ServerResponse(
@field:JsonProperty("id") val id: kotlin.Int,
@field:JsonProperty("purpose") val purpose: ServerResponse.Purpose,
@field:JsonProperty("latency") val latency: kotlin.Int,
@field:JsonProperty("bandwidth") val bandwidth: kotlin.Int,
@field:JsonProperty("state") val state: StateInfo
)
Код получился неплохой, но все равно много лишнего, например аннотация @JsonProperty
или enum как inner class.
Чтобы не держать огромную команду запуска перенесем основные параметры в config.yml.
Лишние файлы
Полученный шаблон кода оформлен как проект, т.е. его можно собрать и использовать как модуль или опубликовать в репозиторий. Но наша задача сгенерировать код моделей, а все остальное не нужно
В сгенерированном коде мы получаем большое количество лишних файлов:
build.gradle
,settings.gradle
,gradlew
,gradlew.bat
,gradle/
;README.md
,docs/
- файлы в пакете /org/openapitools/client/infrastructure/
Опишем эти исключения в файле .openapi-generator-ignore.
$ openapi-generator generate \
-g kotlin \
--config openapi/client/config.yml \
--ignore-file-override openapi/client/.openapi-generator-ignore \
-o client/build/generated \
-i openapi/servers.yml
На этот раз мы убрали все лишние файлы:
Используем свои шаблоны генерации кода по OpenAPI
Структура шаблона
Мы избавились от лишних файлов, теперь займемся кастомизацией шаблона. В качестве движка шаблонизации используется mustache.
В OpenAPI generator можно переопределить часть шаблона генерации, при этом остальные файлы оставить без изменений.
Для начала надо выгрузить шаблон:
$ openapi-generator author template -g kotlin --library jvm-ktor -o openapi/templates
Заходим в openapi/client/templates и видим большое количество шаблонов:
Файлов много, но по сути есть 5 входных типов файлов, а остальные просто являются частью других шаблонов:
- API – клиент или сервер;
- APIDocs – markdown описание API;
- Model – модели;
- ModelDocs – markdown описание моделей;
- SupportingFiles – дополнительные файлы.
Нам нужна кастомизация моделей, поэтому удаляем лишнее и оставляем только три файла:
- common model template – общий шаблон модели;
- data class –
data class
; - enum –
enum
.
Для примера рассмотрим шаблон model
:
package
import
- `` – подключение шаблона;
- `` – обращение к значению переменной;
- ` …. ` – обращение к переменной (так же используется для обхода списка).
Т.е. в этом примере подключается шаблон licenseInfo.mustache
, а
потом, если модель – enum, то подключаем шаблон enum_class.mustache
,
иначе data_class.mustache
.
data class (
var : SetListArray<}}>}}? = null,
)
OpenAPI generator разбирает OpenAPI и собираем объект, который передает в шаблонизатор:
{
"importPath": "ru.romanow.openapi.client.models.CreateServerRequest",
"model": {
"name": "CreateServerRequest",
"classname": "CreateServerRequest",
"isPrimitiveType": false,
"vars": [
{
"openApiType": "string",
"dataType": "kotlin.String",
"name": "purpose",
"baseType": "kotlin.String",
"required": true,
"deprecated": false,
"isPrimitiveType": true,
"isContainer": false,
"isString": true,
"isNumeric": false,
"isInteger": false,
"isShort": false,
"isLong": false,
"isUnboundedInteger": false,
"isNumber": false,
"isFloat": false,
"isDouble": false,
"isDecimal": false,
"isByteArray": false,
"isBinary": false,
"isFile": false,
"isBoolean": false,
"isDate": false,
"isDateTime": false,
"isUuid": false,
"isEmail": false,
"isPassword": false,
"isNull": false,
"isVoid": false,
"isFreeFormObject": false,
"isAnyType": false,
"isArray": false,
"isMap": false,
"isEnum": false,
"isInnerEnum": false,
"isEnumRef": false,
"isReadOnly": false,
"isWriteOnly": false,
"isNullable": false,
"vars": [],
"requiredVars": [],
"hasValidation": false,
"isInherited": false,
"nameInCamelCase": "Purpose",
"nameInSnakeCase": "PURPOSE",
"datatype": "kotlin.String",
"hasItems": false,
"isEnumOrRef": false
}
]
}
}
В config.yml указываем измененный шаблон и при запуске OpenAPI Generator передаем папку с шаблонами:
files:
model.mustache:
templateType: Model
destinationFilename: .kt
$ openapi-generator generate \
-g kotlin \
--config openapi/client/config.yml \
--ignore-file-override openapi/client/.openapi-generator-ignore \
--template-dir openapi/client/templates \
-o client/build/generated \
-i openapi/servers.yml
Если нам не требуется модифицировать шаблон (например ApiController в server), то мы просто его не меняем и он создается на основе базовых шаблонов.
Если требуется убрать сгенерированные файлы, то описываем их в .openapi-generator-ignore.
Вместо выводов: как поддержать баланс между чувством прекрасного и сгенерированным кодом?
- Если разработка идет в одной команде, то возможно Code First вам подойдет, т.к. для него не требуется никаких особенных настроек.
- Если все-таки вы работаете с Contract First, то нужно смотреть в сторону codegen: без этого реализация быстро разойдется с контрактом.
- OpenAPI generator позволяет кастомизировать шаблоны, следовательно, вы можете сгенерировать код, удовлетворяющий Code Style и вашему чувству прекрасного.
Вместо выводов: как максимально быстро реагировать в тестах на изменения в коде?
- Возможность генерировать модели и клиента по OpenAPI позволяет вам быстрее реагировать на изменения в реализации.
- OpenAPI generator сгенерирует за вас весь boilerplate, вам же останется лишь описать сценарии.
- OpenAPI generator позволяет кастомизировать шаблоны, следовательно, вы можете сгенерировать код, удовлетворяющий Code Style и вашему чувству прекрасного.