Многие компании перешли от монолитов к микросервисам для лучшей масштабируемости и ускорения циклов разработки, как и на Kiwi.com . У нас все еще есть монолитные приложения, однако они со временем тают, и множество блестящих микросервисов постепенно вытесняет их.
Эти новые микросервисы используют схемы Open API, чтобы объявить свои контракты и четко заявить о своих ожиданиях. Схемы дают множество преимуществ, таких как автоматически генерируемые клиенты, интерактивная документация, и помогают контролировать взаимодействие приложений друг с другом.
Общение между сервисами становится более сложным, когда число участников растет, и в этой статье я хотел бы поделиться своими мыслями о проблемах использования схем в веб-приложениях и наметить некоторые способы, с которыми мы можем бороться с ними.
Вам также может понравиться: Начало работы со спецификацией OpenAPI
Несмотря на то, что Open API превосходит своего предшественника, Swagger , он имеет множество ограничений, а также любую спецификацию схемы. Основная проблема заключается в том, что даже если объявленная схема отражает все, что подразумевает автор, это не означает, что фактическое приложение соответствует схеме.
Есть много разных подходов, которые пытаются синхронизировать схемы, приложения и документацию. Наиболее распространенные подходы:
- Хранить отдельно, синхронизировать вручную;
- Создать схему из приложения (например,
apispec
); - Предоставить логику приложения из объявленной схемы (
connexion
).
Ни один из этих подходов не гарантирует 1: 1 соответствия поведения приложения и его схемы, и для этого есть много причин. Это может быть сложное ограничение базы данных, которое не может быть выражено в синтаксисе схемы или вездесущий человеческий фактор — либо мы забыли обновить приложение, чтобы оно отражало его схему, либо наоборот.
Последствия этих проблем несоответствия являются множеством, от неуправляемой ошибки, которая приводит к сбою приложения к проблемам безопасности, которые могут привести к финансовым потерям.
Простым способом решения этих проблем является тестирование приложений и настройка статических линтеров для схемы (например, Zally из Zalando), что у нас много, но все становится сложнее, когда вам нужно работать с сотнями сервисов различных типов. размеры.
Классические, основанные на примерах тесты требуют затрат на обслуживание и требуют времени для написания, но они по-прежнему имеют решающее значение для любого современного рабочего процесса разработки. Мы искали дешевый и эффективный способ поиска дефектов в разрабатываемых нами приложениях, который позволит нам тестировать приложения, написанные на любом языке, требовать минимального обслуживания и будет прост в использовании.
Для этого мы решили исследовать применимость тестирования на основе свойств (PBT) к схемам Open API. Сама концепция не нова. Впервые он был реализован в библиотеке Haskell под названием QuickCheck Коеном Клессеном и Джоном Хьюзом. В настоящее время инструменты PBT реализованы на большинстве языков программирования, включая Python, наш основной внутренний язык. В приведенных ниже примерах я собираюсь использовать гипотезу Дэвида Р. Макивера.
Смысл этого подхода состоит в том, чтобы определить свойства, которым должен удовлетворять код, и проверить, что свойства хранятся в большом количестве случайно сгенерированных случаев. Давайте представим простую функцию, которая принимает два числа, возвращает их сумму и тест для нее. Мы ожидаем от этого кода, что реализованное дополнение обладает коммутативным свойством:
питон
1
def add(a, b):
2
return a + b
3
def test_add(a, b):
6
assert add(a, b) == add(b, a)
Однако, Hypothesis
может быстро напомнить нам, что коммутативность имеет место только для действительных чисел:
питон
xxxxxxxxxx
1
from hypothesis import strategies as st, given
2
NUMBERS = st.integers() | st.floats()
4
a=NUMBERS, b=NUMBERS) (
7
def test_add(a, b):
8
assert add(a, b) == add(b, a)
9
# Falsifying example: test_add(a=0, b=nan)
PBT позволяет разработчикам находить множество неочевидных случаев, когда код не работает должным образом, так как его можно применить к схемам API?
Оказывается, мы ожидаем довольно многого от наших приложений. Им следует:
- соответствуют их схемам;
- не сбой ни на одном входе, действительном или недействительном;
- иметь время отклика не более нескольких сотен миллисекунд;
Соответствие схемы может быть расширено:
- Допустимый ввод должен быть принят, неверный ввод должен быть отклонен;
- Все ответы имеют ожидаемый код состояния;
- Все ответы имеют ожидаемый заголовок типа контента.
Несмотря на то, что невозможно сохранить эти свойства во всех случаях, эти пункты все еще являются хорошими целями. Сами схемы являются источником ожидания от приложения, что делает их идеально подходящими для PBT.
Прежде всего, мы осмотрелись и обнаружили, что для этого уже есть библиотека Python swagger-conformance
, но она, похоже, заброшена. Нам нужна была поддержка Open API и больше гибкости со стратегиями генерации данных, чем swagger-conformance
было. Мы также нашли недавнюю библиотеку hypothesis-jsonschema
, созданную одним из Hypothesis
разработчиков ядра, Zac Hatfield-Dodds. Я полностью благодарен людям, которые нашли время для разработки этих библиотек. Благодаря их усилиям тестирование в Python стало более захватывающим, вдохновляющим и приятным.
Поскольку Open API основан на JSON Schema, это было близкое совпадение, но не совсем то, что нам было нужно. Имея эти данные, мы решили создать свою собственную библиотеку на вершине гипотезы, hypothesis-jsonschema
и pytest
, которая будет направлена на Open API и спецификацию чванства.
Вот как проект Schemathesis начался несколько месяцев назад в нашей команде тестирования платформы на Kiwi.com. Идея состоит в том, чтобы:
- Конвертировать определения Open API и Swagger в схему JSON;
- Используйте,
hypothesis-jsonschema
чтобы получить правильные стратегии гипотез; - Используйте эти стратегии в CLI и рукописных тестах.
Он генерирует тестовые данные, которые соответствуют схеме, и делает соответствующий сетевой вызов работающему приложению и проверяет, происходит ли сбой или соответствует ли полученный ответ схеме.
У нас еще есть много интересных вещей для реализации, таких как:
- Генерация неверных данных;
- Генерация схемы из других спецификаций;
- Генерация схемы из приложений WSGI;
- Инструменты на стороне приложения для лучшей отладки;
- Целевое расплывание данных на основе покрытия кода или других параметров.
Даже сейчас это помогло нам улучшить наши приложения и бороться с определенными классами дефектов. Позвольте мне показать вам несколько примеров того, как работает Schemathesis и какие ошибки он может найти. Для этого я создал пример проекта, который реализует простой API для бронирований, исходный код здесь . У него есть недостатки, которые обычно не так очевидны с первого взгляда, и мы найдем их с помощью Схематеза.
Есть две конечные точки:
POST /api/bookings
- создает новое бронированиеGET /api/bookings/{booking_id}/
- получить заказ по ID
В остальной части статьи я предполагаю, что этот проект запущен 127.0.0.1:8080
.
Схема может быть использована в качестве приложения командной строки или в тестах Python; оба варианта имеют свои преимущества и недостатки, и я упомяну их в следующих нескольких параграфах.
Давайте начнем с CLI, и я постараюсь создать новое бронирование. Модель бронирования имеет всего несколько полей, которые описаны в следующей схеме и таблице базы данных:
YAML
xxxxxxxxxx
1
components
2
schemas
3
Booking
4
properties
5
id
6
type integer
7
name
8
type string
9
is_active
10
type boolean
11
type object
SQL
xxxxxxxxxx
1
CREATE TABLE bookings (
2
id INTEGER PRIMARY KEY,
3
name VARCHAR(30),
4
is_active BOOLEAN
5
);
Обработчики и модели:
питон
xxxxxxxxxx
1
# models.py
2
import attr
3
s(slots=True) .
5
class Booking:
6
id: int = attr.ib()
7
name: str = attr.ib()
8
is_active: bool = attr.ib()
9
asdict = attr.asdict
11
# db.py
13
from . import models
14
async def create_booking(pool, *, booking_id: int, name: str, is_active: bool) -> models.Booking:
16
row = await pool.fetchrow(
17
"INSERT INTO bookings (id, name, is_active) VALUES ($1, $2, $3) RETURNING *",
18
booking_id, name, is_active
19
)
20
return models.Booking(**row)
21
# views.py
23
from aiohttp import web
24
from . import db
25
async def create_booking(request: web.Request, body) -> web.Response:
28
booking = await db.create_booking(
29
request.app["db"], booking_id=body["id"], name=body["name"], is_active=body["is_active"]
30
)
31
return web.json_response(booking.asdict())
Вы заметили ошибку, которая может привести к сбою приложения с необработанной ошибкой?
Нам нужно запустить Schemathesis для конкретной конечной точки нашего API:
Оболочка
xxxxxxxxxx
1
$ schemathesis run
2
-M POST
3
-E /bookings/
4
http://0.0.0.0:8080/api/openapi.json
Эти два варианта, --method
и —-endpoint
позволяют запускать тесты только на конечных точках, которые интересны для Вас.
CLI Schemathesis создаст простой код Python, чтобы вы могли легко воспроизвести ошибку и запомнит ее во внутренней базе данных гипотез, поэтому она будет использоваться в последующем запуске. Трассировка в выходных данных сервера раскрывает проблемный параметр:
Простой текст
xxxxxxxxxx
1
File "/example/views.py", line 13, in create_booking
2
request.app["db"], booking_id=body["id"], name=body["name"], is_active=body["is_active"]
3
KeyError: 'id'
Исправление простое: нам нужно внести id
и другие параметры, необходимые в схеме:
YAML
xxxxxxxxxx
1
components
2
schemas
3
Booking
4
properties
5
id
6
type integer
7
name
8
type string
9
is_active
10
type boolean
11
type object
12
required id name is_active
Давайте еще раз запустим последнюю команду и проверим, все ли в порядке:
Еще раз! Исключение на стороне приложения:
Простой текст
xxxxxxxxxx
1
asyncpg.exceptions.UniqueViolationError: duplicate key value violates unique constraint "bookings_pkey"
2
DETAIL: Key (id)=(0) already exists.
Похоже, я не учел, что пользователь может попытаться создать одно и то же бронирование дважды! Однако подобные вещи распространены в производстве - двойной щелчок, повторная попытка при сбое и т. Д.
Мы часто не можем представить, как наши приложения будут использоваться после развертывания, но PBT может помочь обнаружить, какая логика отсутствует в реализации.
В качестве альтернативы, Schemathesis предоставляет способ интегрировать свои функции в обычные тестовые наборы Python. Другая конечная точка может показаться простой - взять объект из базы данных и сериализовать его, но он также содержит ошибку.
YAML
xxxxxxxxxx
1
paths
2
/bookings/{booking_id}
3
parameters
4
description Booking ID to retrieve
5
in path
6
name booking_id
7
requiredtrue
8
schema
9
format int32
10
type integer
11
get
12
summary Get a booking by ID
13
operationId example.views.get_booking_by_id
14
responses
15
"200"
16
description OK
питон
xxxxxxxxxx
1
# db.py
2
async def get_booking_by_id(pool, *, booking_id: int) -> Optional[models.Booking]:
3
row = await pool.fetchrow(
4
"SELECT * FROM bookings WHERE id = $1", booking_id
5
)
6
if row is not None:
7
return models.Booking(**row)
8
# views.py
10
async def get_booking_by_id(request: web.Request, booking_id: int) -> web.Response:
11
booking = await db.get_booking_by_id(request.app["db"], booking_id=booking_id)
12
if booking is not None:
13
data = booking.asdict()
14
else:
15
data = {}
16
return web.json_response(data)
Центральным элементом использования кода в Schemathesis является экземпляр схемы. Он обеспечивает параметризацию теста, выбор конечных точек для тестирования и другие параметры конфигурации.
Существует несколько способов создания схемы, и все они могут быть найдены в schemathesis.from_<something>
шаблоне. Обычно гораздо лучше иметь приложение в качестве приспособления pytest, чтобы оно могло запускаться по требованию (и schemathesis.from_pytest_fixture
поможет сделать это), но для простоты я буду следовать своему предположению о приложении, работающем локально на порту 8080 :
питон
xxxxxxxxxx
1
import schemathesis
2
schema = schemathesis.from_uri("http://0.0.0.0:8080/api/openapi.json")
4
parametrize(method="GET", endpoint="/bookings/{booking_id}") .
6
def test_get_booking(case):
7
response = case.call()
8
assert response.status_code < 500
Каждый тест с этим schema.parametrize
декоратором должен принимать case
прибор, который содержит атрибуты, требуемые схемой, и дополнительную информацию для выполнения соответствующего сетевого запроса. Это может выглядеть так:
питон
xxxxxxxxxx
1
>>> case
2
Case(
3
path='/bookings/{booking_id}',
4
method='GET',
5
base_url='http://0.0.0.0:8080/api',
6
path_parameters={'booking_id': 2147483648},
7
headers={},
8
cookies={},
9
query={},
10
body=None,
11
form_data={}
12
)
Case.call
отправляет запрос с этими данными в работающее приложение через requests
.
И тесты можно запускать с помощью pytest (но unittest
также поддерживается стандартная библиотека):
$ pytest test_example.py -v
Исключение на стороне сервера :
Простой текст
xxxxxxxxxx
1
asyncpg.exceptions.DataError: invalid input for query argument $1: 2147483648 (value out of int32 range)
Выходные данные обозначают проблему отсутствия проверки входного значения, исправление состоит в том, чтобы указать правильные границы для этого параметра в схеме. Наличие format: int32
не достаточно для проверки значения, согласно спецификации, это только подсказка.
Пример приложения упрощен и не имеет многих производственных свойств, таких как авторизация, отчеты об ошибках и т. Д. Тем не менее, схема и тестирование на основе свойств, в целом, могут обнаружить широкий спектр проблем в приложениях. Вот краткое изложение выше и еще несколько примеров:
- Отсутствует логика для необычных сценариев;
- Повреждение данных (из-за отсутствия проверки);
- Отказ в обслуживании (например, из-за плохо составленных регулярных выражений);
- Ошибки в клиентских реализациях (из-за разрешения неперечисленных свойств во входных значениях);
- Другие ошибки, которые вызывают сбой приложения.
Эти проблемы имеют различные уровни серьезности, однако даже небольшие сбои засоряют ваш трекер ошибок и появляются в ваших уведомлениях. Я столкнулся с некоторыми ошибками, такими как производственные, и я предпочитаю исправлять их раньше, чем когда меня разбудил вызов PagerDuty посреди ночи!
Есть много вещей, чтобы улучшить их в областях, и я хотел бы призвать вас внести свой вклад в Схематез, Гипотезу, гипотезу-jsonschema или Pytest, все из которых с открытым исходным кодом.
Спасибо за Ваше внимание!
Дайте мне знать в комментариях о любых ваших отзывах или вопросах, или присоединяйтесь к нашему чату на Gitter .