07.09.2021       Выпуск 403 (06.09.2021 - 12.09.2021)       Статьи

Использование API-схем для property-based-тестирования

Когда мы работаем с API-схемами, обычно существует несколько моделей, и они синхронизируются на разных уровнях. Обычно есть база данных, код и схема. И всё это нужно держать между собой в синхроне, чтобы они нормально друг с другом взаимодействовали.

Я расскажу об обычных проблемах, с которыми люди сталкиваются при использовании API-схем. Как можно использовать API-схемы для описания property-based-тестов, и чем здесь может помочь Schemathesis. И покажу на практике, как его можно интегрировать в существующий проект.

Читать>>




Экспериментальная функция:

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

Просим обратить внимание, что текст по ссылке и здесь может не совпадать.

Когда мы работаем с API-схемами, обычно существует несколько моделей, и они синхронизируются на разных уровнях. Обычно есть база данных, код и схема. И всё это нужно держать между собой в синхроне, чтобы они нормально друг с другом взаимодействовали.

Я расскажу об обычных проблемах, с которыми люди сталкиваются при использовании API-схем. Как можно использовать API-схемы для описания property-based-тестов, и чем здесь может помочь Schemathesis. И покажу на практике, как его можно интегрировать в существующий проект.

Проблемы использования API-схем

Ручная синхронизация

Пример простой таблички с букингами:

Здесь есть три поля, какой-то Python-класс и фрагмент схемы Open API, которая это как-то отображает. В принципе, здесь уже есть ошибка, и мы ее сейчас разберем.

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

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

Недостаточное тестирование

При этом часто код тестируется недостаточно. Обычно пишут больше example-based-тестов: такой-то вход, ожидаем такой-то выход. Еще используются различные инструменты вроде Dredd, когда в схеме указываются примеры и прогоняются на валидность, практически не прибегая к генерации данных.

Еще можно параметризовать существующие example-based-тесты, чтобы понизить стоимость поддержки. Но это всё еще довольно дорого, занимает много времени и не всегда эффективно. Если мы пишем тесты руками, то сфера покрытия ограничивается только нашим воображением. Что означает — не всегда можно заметить крайние случаи и указать достаточное количество примеров.

Property-based тестирование

С другой стороны, существует альтернативный подход — property-based тестирование. Здесь мы вместо того, чтобы указать конкретный вход, говорим, что эта функция принимает данные такого-то типа и должны выполняться такие-то свойства.

Какие это свойства? В целом, нам нужно, чтобы само приложение отвечало быстро, а все примеры в схемах были актуальными. А для API-схем нам важно, чтобы поведение приложения соответствовало описанной схеме. Это наша основная цель — чтобы всё работало, как описано. При этом любые входные данные, как корректные, так и некорректные, не должны вызывать внутренних ошибок. Особенно это важно, когда у вас есть on call и ошибка в Sentry вызывает звонок на ваш мобильник через Pagerduty.

На мой взгляд, API-схемы — отличный источник таких свойств.

Schemathesis

Я разрабатываю Schemathesis, который позволяет при наличии только API-схемы, причем необязательно валидной на 100%, генерировать property-based-тесты и запускать их. Schemathesis поддерживает Open API 2 & 3, а также GraphQL.

С его помощью можно сгенерировать все описанные в схеме запросы а так же проверить все примеры из схем. Даже если у вас не хватает примера для какого-то поля, он его сгенерирует. И, в отличие от большинства подобных инструментов, Schemathesis не только пригоден для юнит-тестирования, но и поддерживает интеграционное тестирование.

Например, у вас есть заказы. Тогда мы создаем заказ, берем ID, смотрим, что API отдает корректный ответ, обновляем заказ, удаляем заказ. Можно создавать целые цепочки API запросов.

Он доступен как Python-библиотека, так и в виде command line утилиты. Сейчас в процессе разработки Web-сервис (альфа релиз будет через пару недель), чтобы все это было в один клик.

Schemathesis: демо

Видео показа демо можно посмотреть здесь, а сама демка — по этой ссылке.

Возьмем простое приложение, состоящее буквально из трех эндпойнтов.

openapi.yaml
openapi: 3.0.0
info:
  title: Booking API
  description: A flights booking system.
  version: 1.0.0
servers:
  - url: http://127.0.0.1:5000/api
paths:
  /bookings/{booking_id}:
    parameters:
      - description: Booking ID to retrieve
        in: path
        name: booking_id
        required: true
        schema:
          format: int32
          type: integer
    get:
      summary: Get a booking by ID
      operationId: app.views.get_booking_by_id
      responses:
        "200":
          description: OK
  /bookings/:
    post:
      summary: Create a new booking
      operationId: app.views.create_booking
      responses:
        "201":
          description: OK
          links:
            GetBookingById:
              operationId: app.views.get_booking_by_id
              parameters:
                booking_id: '$response.body#/id'
      requestBody:
        $ref: '#/components/requestBodies/Booking'
    get:
      summary: Get bookings
      operationId: app.views.get_bookings
      parameters:
        - description: Number of bookings to return
          in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 2147483647
      responses:
        "200":
          description: OK
components:
  requestBodies:
    Booking:
      required: true
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Booking'
  schemas:
    Booking:
      properties:
        id:
          type: integer
        name:
          type: string
        is_active:
          type: boolean
      type: object

У меня в Docker оно запущено на порту 5000. При помощи небольшого количества полей мы можем создавать букинги, получать список букингов, и получать букинг по ID. И у нас есть три поля, integer, string и boolean. Я покажу на примере нескольких простых тестов, как это можно всё интегрировать.

Создаем маленький тестовый файл и импортируем Schemathesis. В нем будут обычные Python-тесты, очень близкие к тому, что мы привыкли видеть в Pytest и Hypothesis.

import schemathesis
schema = schemathesis.from_uri(
    "http://127.0.0.1:5000/api/openapi.json"
)

Загружаем схему. Она живет на порту 5000 по пути /api/openapi.json. Схему можно грузить из разных мест: по ссылке или просто передать в словарик, из файла, с какого-то пути или WSGI приложение передать и адрес в нем. Можно из pytest фикстуры, если у нас уже есть существующий набор тестов.

...
@schema.parametrize()
def test_app(case):
    ...

Обыкновенный тест может выглядеть так. Мы на манер pytest.mark.parametrize указываем декоратор и пишем тест. Все аргументы, что нужно указать — это существующие pytest fixtures, которые вы хотите использовать. Обязательно нужно указать case в качестве аргумента. Грубо говоря, это test case для того, чтобы отправить его вашему приложению.

Тест целиком
@schema.parametrize(
    operation_id="app.views.create_booking"
)
def test_app(case):
    response = case.call()
    case.validate_response(response)

Мы не будем тестировать сразу все эндпойнты, а протестируем только создание букинга. Его можно выбрать по operationId. Обычно тест — это отправка запроса, получение ответа и какая-то валидацию. Schemathesis может провести 5 встроенных проверок:

  1. Что это правильный код ответа из указанных в схеме;

  2. Что content-type у него корректный;

  3. Что структура тела ответа соответствует схеме;

  4. Что все необходимые заголовки присутствуют в ответе;

  5. Что это не пятисотка, что у нас ничего не упало.

Этот делается довольно просто, и, если его запустить через pytest test, то сразу всё упадет.

Видим, что вьюха упала с KeyError. Такие ситуации встречаются, потому что id может отсутствовать, согласно нашей схеме. Мы его не указали, как обязательный. Давайте это сделаем, и все остальные поля тоже, потому что они сделаны таким же образом.

Указываем обязательные поля
schemas:
    Booking:
      properties:
        id:
          type: integer
        name:
          type: string
        is_active:
          type: boolean
      type: object
      required: ["id", "name", "is_active"]

Теперь перезапустим приложение и посмотрим, что происходит.

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

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

Простой хак
async def create_booking(
    request: web.Request, 
    body
) -> web.Response:
    try:
        booking = await db.create_booking(
            request.app["db"],
            booking_id=body["id"],
            name=body["name"],
            is_active=body["is_active"],
        )
        return web.json_response(booking.asdict(), status=201)
    except asyncpg.UniqueViolationError:
        return web.json_response(
            {"message": "Booking already exists"}, 
            status=409
        )

ОК. Надеюсь, что сейчас все заработает.

Но нет, я забыл указать 409 в схеме. Schemathesis очень любит напоминать о каких-то пропущенных кусочках. Добавляем код ответа в схему и снова запускаем. Уже больше тестов генерится, но теперь у нас что-то другое произошло.

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

Ограничиваем числовое поле
schemas:
 Booking:
   properties:
     id:
       type: integer
       minimum: 1
       maximum: 2147483647

Хорошо, сейчас у нас все вроде должно быть отлично.

По умолчанию Schemathesis генерит 100 тестов. Он повторяет поведение Hypothesis. Мы можем это поменять при помощи, например, settings-декоратора, который существует в Hypothesis. У нас это классический на самом деле тест, мы можем указать, что max_examples=1000. Тысяча — это верхний лимит, потому что если ваш API, например, принимает boolean и у него всего 2 значения, он сгенерирует 2 значения, тысячу там неоткуда брать.

from hypothesis import settings
...
@schema.parametrize(...)
@settings(max_examples=1000)
def test_app(case):
    ...

Альтернативный способ — можно у себя в conftest.py указать профиль, если вы, например, запускаете это на CI, у вас много тестов и вы не хотите везде дублировать конфигурацию. Например, регистрируем профиль под названием CI и там будет 1000 examples.

from hypothesis import settings
settings.register_profile("CI", max_examples=1000)

Потом можно указать опцию --hypothesis-profile=CI при запуске pytest, чтобы запускать больше тестов. Чем больше тестов, тем больше вероятность того, что мы найдем какую-то ошибку.

В этот раз PostgreSQL не любит null-байты — хорошо, давайте это поправим, сделаем какую-то простую валидацию, опять же игрушечную. Если у нас null-байт в теле (в name), то вернем 400-ю, чтобы все было валидно.

async def create_booking(request: web.Request, body) -> web.Response:
    if "\x00" in body["name"]:
        return web.json_response({"message": "Invalid name"}, status=400)
    ...

Запускаем, но опять мелькают пятисотки. Hypothesis иногда работает довольно медленно из-за того, что ему нужно минимизировать ошибку. Он нашел какую-то проблему и пытается ее сжать до минимальной ее версии. В данной ситуации он нашел, что такая строка в name будет вызывать какое-то исключение.

Falsifying example: test_app(
    case=Case(body={'id': 1, 'is_active': False, 'name': '0000000000000000000000000000000'}),
)

В нашем случае это — лимит на 30 символов в базе, мы просто это добавим в схему. Теперь все должно быть хорошо.

Добавляем лимит
schemas:
 Booking:
   properties:
    #  ...
    name:
      type: string
      maxLength: 30

Часто кроме простых строк и простых типов мы используем типы специального назначения. Например, мы хотим хранить номера карточек, у которых есть своя семантика. Чтобы это отобразить в схеме, можно указать поле format.

Давайте придумаем какой-нибудь нелепый формат, например, fullname — что мы хотим всегда получать имя-пробел-фамилия.

Новый формат
schemas:
 Booking:
   properties:
    #  ...
    name:
      type: string
      format: fullname
      maxLength: 30

Для проверки этого поля этого я написал маленький валидатор. Он буквально делит входные данные пополам по пробелу. Если не получается поделить на две части, это некорректное имя, а если получается, то имя и фамилия должны быть не пустыми.

Валидируем
def is_valid_name(name: str) -> bool:
    try:
        first, last = name.split(" ")
        return bool(first and last)
    except ValueError:
        return False
...
async def create_booking(
    request: web.Request, 
    body
) -> web.Response:
    if "\x00" in body["name"] or not is_valid_name(body["name"]):
        return web.json_response(
            {"message": "Invalid name"}, 
            status=400
        )
    ...

Если мы попробуем это сгенерировать данные такого формата, то у нас это не получится. По умолчанию, Schemathesis будет генерировать просто строки произвольного формата длиной 30 (это максимум). Но он ничего не знает о том, что мы ожидаем имя из двух частей, разделенных пробелом. Чтобы это сделать, мы должны сказать ему об этом.

Чтобы генерировать строку из двух частей, мы можем это сделать при помощи Hypothesis, и потом скормить это Schemathesis. Чтобы последний знал, что ему надо генерировать, когда он встречает формат fullname.

Пишем стратегию Hypothesis
# conftest.py
from hypothesis import strategies as st
@st.composite
def fullname(draw):
    first = draw(st.sampled_from(["john", "jane"]))
    last = draw(st.just("doe"))
    return f"{first} {last}"

В данной ситуации нам понадобится composite — это тип довольно гибкой Hypothesis-стратегии, которая позволяет просто писать функцию и в ней создавать какое-то значение. Для простоты сделаем следующее: возьмем предопределенные имена, например, John и Jane, и также фамилию, но уже один вариант. В качестве результата вернем имя и фамилию, разделенные пробелом.

# conftest.py
import schemathesis
...
schemathesis.register_string_format("fullname", fullname())

Теперь можно через schemathesis.register_string_format зарегистрировать формат fullname. Он должен в точности повторять то, что у нас написано в названии формата в схеме, и передать в него эту вызванную функцию fullname, чтобы она стала стратегией Hypothesis. В выхлопе приложения у нас 409-я, мы прошли валидацию.

Кроме форматов для строк мы можем менять любые части, которые отправляются на сервер — заголовки, query, body.

Попробуем поменять какие-нибудь части запроса. Schemathesis имеет систему хуков, похожую на Pytest. Также нужно использовать декоратор и функцию с определенным именем. Если в данной ситуации мы хотим поменять id в сгенерированных данных, нам нужен хук before_generate_body, который принимает контекст и существующую стратегию, а вернуть должен только модифицированную стратегию. Здесь можно сделать практически что угодно, например, фильтровать, добавлять какие-то варианты. Например, попробуем стратегию, в которой все id будут больше 10 000.

# conftest.py
@schemathesis.hooks.register
def before_generate_body(context, strategy):
    return strategy.filter(lambda x: x.get("id", 10001) > 10000)

Добавляем лямбду, которая принимает сгенерированное значение. У нас есть словарь, в нем поле id. Устанавливаем условие, что id больше 10 000, и проверим, что тест это получает в нужном виде. И да, он не падает, все замечательно.

@schema.parametrize(...)
@settings(max_examples=1000)
def test_app(case):
    assert case.body["id"] > 10000
    ...

Кроме этого можно модифицировать наш case, как нам угодно. Case — это просто хранилище данных. Там есть body, query и все прочее. Обычно, когда мы работаем с каким-то API, там есть авторизация. Нам нужен какой-то заголовок. Попробуем сделать фикстуру, которая возвращает токен.

@pytest.fixture
def token():
    return "spam"

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

Это обычный тест. Мы просто добавляем заголовки. Они пустые в нашем case, потому что мы ничего там не генерируем, в схеме ничего такого нет. Можно просто указать авторизацию, что это Bearer с таким-то токеном.

@schema.parametrize(...)
def test_app(case, token):
    case.headers = {"Authorization": f"Bearer {token}"}
    ...

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

Теперь переключимся на Command Line. Кроме обычных тестов на Python, можно использовать Command Line entrypoint, который вообще не требует никакого кода. Мы просто запускаем Schemathesis и указываем адрес нашей схемы:

schemathesis run http://127.0.0.1:5000/api/openapi.json

Он проходит по всем эндпойнтам. Выберем, например эндпойнт get_bookings. Schemathesis умеет искать по подстроке, поэтому нам хватит только get_bookings.

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

Попробуем написать свою маленькую проверку через декоратор, что наше приложение функционирует достаточно быстро вне зависимости от того, какие данные мы в него отослали. Применим декоратор register_check к новой функции not_so_slow принимающей ответ приложения и тот же case, который у нас был в тесте.

@schemathesis.register_check
def not_so_slow(response, case):
    assert response.elapsed < timedelta(milliseconds=100), "Response is slow!"

Теперь давайте решим, что response.elapsed будет меньше какой-то time delta, например, 100 мс в этой ситуации. И если что, мы получим сообщение «Response is slow!».

Теперь, чтобы это всё подхватилось нашим Command Line инструментом, надо добавить опцию --pre-run=test.hooks перед run. Значением должен быть импортируемый путь к модулю, в котором вы определяете всю кастомизацию, которая вам нужна — где живут ваши string-форматы, кастомные стратегии, фильтрации, чеки. Всё это здесь.

schemathesis --pre-run=test.hooks run http://127.0.0.1:5000/api/openapi.json

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

schemathesis --pre-run=test.hooks run http://127.0.0.1:5000/api/openapi.json --target=response_time -c not_so_slow

Запускаем и видим, что тесты идут все еще медленно. Schemathesis пытается минимизировать значения лимита. Он нащупал, что в районе 900 уже начинает замедляться, и 280 мс — это долго. Эта штука неустойчивая — где-то отвечает медленнее, где-то быстрее, но примерно граница находится в этом районе.

Что можно сделать? Можно сказать, что просить будем не больше 200 букингов из API, это и так сильно много. Давайте попробуем. И теперь тесты бегут быстро.

Таким образом можно искать части API, которые подвержены denial-of-service атакам. Например, у вас могут генериться данные, которые будут грузить приложение или выполняться какие-то слишком тяжёлые операции. Найти довольно легко, просто зарегистрировав такой чек, а данные он сгенерит сам. Можно максимизировать не время, а, например, размер ответа.

Покажу, как можно это сделать. В функции можно посчитать длину context.response.content, и вернуть её как float. Так мы максимизируем размер ответа. Эффект лучше виден на большом количестве тестов, потому что Hypothesis нужно больше данных, чтобы направить генерацию. Тем не менее это работает, общее поведение повторяемо.

# conftest.py
@schemathesis.register_target
def big_response(context):
    return float(len(context.response.content))

и

schemathesis --pre-run=test.hooks run http://127.0.0.1:5000/api/openapi.json --target=big_response -c not_so_slow

Кроме этого можно в наших CLI тестах указать количество воркеров на которые будут распараллелены тесты. Тогда всё может пойти быстрее, хотя и зависит от ситуации. Также можно указать WSGI приложение, ASGI приложение. Все работает из коробки.

schemathesis --pre-run=test.hooks run http://127.0.0.1:5000/api/openapi.json -w 4

Переключимся на интеграционное тестирование. По умолчанию большинство инструментов сосредотачиваются на тестирование одного эндпойнта в изоляции. В Schemathesis можно генерировать последовательности запросов и тестировать сразу поведение целиком в рамках системы. В данный момент он поддерживает только один способ — через Open API Links.

Что такое Open API Links? Он позволяет связать выхлоп одного эндпойнта с другим, и указать — как именно. Посмотрим на примере ответ 201 при создании букинга. Берем произвольное имя, например, GetBookingById, и указываем operationId, то есть какие операции мы связываем. Нам надо указать все параметры, которые указаны в эндпойнте в ключе parameters. У нас он всего один — это booking_id, его и указываем. Здесь Open API использует так называемый runtime expression, где можно при помощи специального синтаксиса достать данные из ответа или из запроса, который вы отсылали.

API Links
/bookings/:
 post:
   summary: Create a new booking
   operationId: app.views.create_booking
   responses:
     "201":
       description: OK
       links:
         GetBookingById:
           operationId: app.views.get_booking_by_id
           parameters:
             booking_id: '$response.body#/id'

Возьмем тело ответа. Здесь у нас JSON-пойнтер, и нам надо просто взять поле id. Перезагрузим приложение, чтобы линки были видны в схеме. Запустим вместе с параметром --stateful=links. Хотя в данный момент это единственный вариант, но в процессе разработки автоматическое распознавание линков такого рода. Ребята из Red Hat это реализовали. Они взяли Schemathesis, расширили его и прикрутили inference для этих связей. И теперь он автоматически определяет с какой-то вероятностью линки между разными эндпойнтами и генерит их в нужные запросы без указания Open API links из схемы. Это скоро будет в Schemathesis, но пока только линки.

Чтобы не было выхлопа от других, надо указать только один operationId (create_booking). Смотрим, что получилось.

Schemathesis пытается создать букинги, и для всех 201 ответов делает дополнительные запросы на GET /bookings/{booking_id} с таким id. Понятное дело, они не все 201, есть 409, но, тем не менее видим, что их достаточно.

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

Но это Schemathesis CLI, который в целом имеет больше фич на данный момент, и не все вещи сделаны еще для обычных Python тестов. В основном это связано с Pytest — некоторые вещи там требуют большей работы, чем написать для CLI с нуля.

Попробуем написать какую-то альтернативу. У нас все-таки есть обычные стратегии Hypothesis, и можно воспользоваться stateful-тестированием, которое в него встроено, или написать руками. Все тесты в Hypothesis используют декоратор given. Он получает стратегии, а потом данные полученные из этих стратегий используются как аргументы тестовой функции.

Сейчас у нас всего два шага, сначала создадим букинг. Схема позволяет взять эндпойнт POST /bookings/ и преобразовать его в стратегию. То же самое сделаем для второго шага, назовем его get.

@given(
    create=schema["/bookings/"]["POST"].as_strategy(),
    get=schema["/bookings/{booking_id}"]["GET"].as_strategy(),
)
def test_stateful(create, get):
    ...

Нам осталось только эмулировать логику, которую мы описали в линках. Берем create, get и делаем запросы, валидируя при этом ответы. Первый ответ — это создание букинга. Если наш response — 201, то мы должны достать ID из тела и добавить его в path_parameters для GET.

@given(
    create=schema["/bookings/"]["POST"].as_strategy(),
    get=schema["/bookings/{booking_id}"]["GET"].as_strategy(),
)
def test_stateful(create, get):
    response = create.call_and_validate()
    if response.status_code == 201:
        get.path_parameters["booking_id"] = response.json()["id"]
        get.call_and_validate()

Давайте его запустим. Пока первый пробежит, посмотрим, что во втором. Он прошел и конечно, здесь много повторных 409. Мы сделали запрос на создание букинга, потом пришел 201, мы взяли ID и отправили его сюда, получили 200 ответ. Всё отлично.

Таким образом можно запускать такого рода тесты, дополнительно используя инструменты из Hypothesis, которые позволяют это расширить. Всё довольно гибко.

В более новых версиях Schemathesis доступны тесты на основе конечных автоматов из Hypothesis, которые обладают дополнительными возможностями, о которых можно прочитать в документации вот тут.

Schemathesis: что дальше?

В Schemathesis мы уже реализовали негативные тесты идобавили улучшенную поддержку GraphOL.

Расскажу об аспектах, когда Schemathesis работает не очень хорошо, и его ограничениях, которые сейчас в работе.

Schemathesis as a Service.Мы работаем над тем, чтобы всё было в один клик. Просто вводим адрес схемы и всё замечательно тестируется, не надо писать никакого кода.

Решить проблему с производительностью при генерации рекурсивных данных.Некоторые схемы при достаточной сложности и достаточном размере могут вызвать реальные проблемы с производительностью при генерации тестов. Над этим сейчас мы с Заком (core-разработчик из Hypothesis) работаем. Тогда можно будет полноценно генерировать рекурсивные данные любой сложности.

Поддержка API Blueprint.Многие используют этот стандарт, и мы его тоже планируем поддерживать.

Coverage-guided генерация данных. Достаточно часто бывают проблемы с качеством данных. Даже если мы передаем корректные ID, не всегда получается пройти достаточно глубоко в код приложения. Для этого можно прикрутить coverage-guided генерацию. У меня было несколько экспериментов, работает довольно успешно. Пока это еще далеко не production-ready, но будет.

Заключение, или зачем я создал Schemathesis

Во-первых, я хотел сделать ошибки более явными. В демо вы видели нулевые байты, ограничения на длину, некорректные данные для базы. Конечно, это может решаться сгенерированной валидацией, схемой, чем-то еще. Но все эти подходы могут сбоить. Я видел много ситуаций, когда ошибка, из-за которой я просыпался ночью от PagerDuty и которую мы в итоге пофиксили, была найдена Schemathesis’ом за 3 секунды. Хотел бы я видеть это раньше.

Во-вторых, я хочу снизить затраты на тестирование, чтобы не было большого количества example-based тестов, чтобы они не занимали много времени у разработчиков и тестировщиков. Чтобы всё было как можно проще. Источник для нужных нам свойств уже есть — это API схемы.

В третьих, я хочу дополнить существующие тесты. Я не позиционирую Schemathesis как то, что заменит example-based тесты. Нет, он их дополняет. Как вы видели, итерироваться получается неплохо. Также он умеет как Dredd проверять примеры, которые есть в схеме, но он не будет ругаться, если что-то отсутствует.

Еще я хочу упростить поддержание схем в актуальном состоянии и поддерживать проект достаточно гибким и расширяемым. Чтобы каждый мог его адаптировать, расширить, добавить свои стратегии для генерации данных, подстроив под себя.

Дайте знать, что вы думаете об инструменте в комментах или на канале в gitter и записывайтесь на альфа тестирование Schemathesis as a Service. Спасибо!

Moscow Python Conf++ 2021 состоится 27-28 сентября в Москве. Доклады можно посмотреть здесь. Билеты можно купить здесь.

Приходите, будет интересно!






Разместим вашу рекламу

Пиши: mail@pythondigest.ru

Нашли опечатку?

Выделите фрагмент и отправьте нажатием Ctrl+Enter.

Система Orphus