YATL — тестирование API по-новому
Abstract
При разработке современного программного обеспечения поддержка интеграционных тестов для веб-API часто становится узким местом.Традиционное написание API-тестов требует написания большого объема вспомогательного кода: настройки сессий, обработки исключений, сериализации данных и т.д. Подход «тесты как данные» предлагает кардинально иную философию, где тест – это не скрипт, а описание того, что нужно проверить. Наборы тестов, написанные на императивных языках программирования, как правило, устаревают быстрее, чем проверяемая кодовая база, поскольку разработчикам и тестировщикам не хватает мотивации для их обновления. Более того, заинтересованным лицам, не имеющим опыта программирования (например, аналитикам, специалистам по ручному контролю качества), нелегко понять такие тесты или внести свой вклад в их проведение. В этой статье рассматривается, может ли язык, ориентированный на предметную область (DSL), решить эти проблемы.
Introduction
YATL — это DSL (domain-specific language), написанный на Python, для тестирования API. Он использует YAML в качестве языка описания тестов. Но это не очередной фреймворк для разработчиков, а инструмент, который демократизирует API-тестирование, делая его доступным для всей команды.
Если вы знаете HTTP и YAML — вы знаете YATL.
Вместо написания императивного кода вы декларативно описываете тесты в YAML-файлах. Давайте сразу посмотрим, как выглядит простейший тест на проверку GET-запроса:
name: ping
base_url: google.com
steps:
- name: access_test
request:
method: GET
expect:
status: 200
Никакой скрытой «магии»: только запрос (метод, URL) и ожидаемый ответ (статус, частичная проверка тела). Это гораздо ближе к спецификации, чем к скрипту. Каждый файл с расширением .test.yaml — это тестовый сценарий, состоящий из нескольких шагов (steps).
А вот как этот тест выглядел бы на Java:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.io.IOException;
public class HttpTest {
public static void main(String[] args) {
try {
test();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
public static void test() throws IOException, InterruptedException {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://google.com"))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
assert response.statusCode() == 200 : "Expected 200, got " + response.statusCode();
System.out.println("All tests passed.");
}
}
Согласитесь, что первый вариант выглядит более дружелюбно и требует значительно меньше подготовительных действий.
Теперь перейдем к ключевым возможностям и особенностям проекта:
Ключевые возможности
Поддержка всех форматов данных
YATL из коробки понимает большинство content-type, с которыми вы столкнётесь в реальной жизни:
-
JSON — автоматически устанавливает Content-Type: application/json
-
XML — для SOAP и других XML-API
-
Form-данные — application/x-www-form-urlencoded
-
Multipart-файлы — загрузка файлов
-
Простой текст — text/plain
Пример с JSON:
request:
method: POST
url: /users
body:
json:
name: "Иван Иванов"
email: "ivan@example.com"
Извлечение данных и шаблонизация
Вы можете извлекать любые фрагменты из ответов и использовать их в последующих запросах через мощные Jinja2-шаблоны. Для доступа к вложенным полям JSON используется точечная нотация (например, user.info.name), что делает синтаксис чистым и кратким: Представьте цепочку:
steps:
- name: создание_пользователя
request:
method: POST
url: /users
body:
json:
name: "Алиса"
expect:
status: 200
extract:
user_id: "response.id" # Извлекаем ID из ответа
- name: получение_пользователя
request:
method: GET
url: "/users/{{ user_id }}" # Используем извлеченный ID
status: 200
json:
user.info.name: "Алиса" # проверка имени, через точечную нотацию
Параллельное выполнение
YATL по умолчанию запускает тесты в 10 потоках (значение можно настроить через --workers). Это кардинально ускоряет прогон больших наборов тестов, например, для регресса.
Пропуск тестов и шагов
В реальной разработке часто нужно временно отключить тест, не удаляя его. YATL поддерживает механизм skip:
name: Тест в разработке
skip: true # Весь тест будет пропущен
Можно пропустить и отдельный шаг:
steps:
- name: активный_шаг
request: ...
- name: пропущенный_шаг
skip: true # Только этот шаг пропускается
request: ...
Интеграция с CI/CD
Настоящая ценность YATL раскрывается, когда вы интегрируете его в CI/CD.
Это позволит:
- Ловить баги до попадания в продакшен — тесты будут запускаться на каждый PR
- Не забывать запускать тесты — это делает CI-сервер
name: API Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.14'
- run: pip install yatl
- run: yatl tests/ --workers 5
Важно: YATL тестирует уже запущенный API. Он не умеет сам поднимать сервер. Поэтому в CI/CD вы должны сначала запустить свой API (с актуальным кодом), и только затем вызывать yatl.
Параметризация тестов
Иногда в тестах требуется проверить одну и ту же логику на различных тестовых данных. Например, рассмотрим тесты которые проверяют работу Google:
name: ping
base_url: google.com
steps:
- name: access_test
request:
method: GET
expect:
status: 200
- name: failed_test
request:
method: GET
url: /not_found
expect:
status: 404
Можно заметить дублирование кода, по сути, тесты не отличаются ничем кроме ожидаемого статуса, и ссылки. В том случае, если бы мы хотели добавить новые проверки в тест (например вызвать ошибку 400), то нам пришлось бы добавлять новый шаг. Тоже самое с удалением и редактированием кода в тестах. Множество одинаковых тестов усложняет поддержку тест кейсов. Давайте разберем как можно избежать дублирование кода с помощью параметризации.
Параметризация — это способ запустить один и тот же тест с разными входными данными. В YATL параметризация реализована через ключевое словое parametrize. Вот как можно использовать параметризацию:
name: Params test
base_url: google.com
steps:
- name: Ping
parametrize:
- path: /
expected_status: 200
- path: /not_found
expected_status: 404
request:
method: GET
url: "{{ path }}"
expect:
status: "{{ expected_status }}"
Почему следует использовать параметризацию:
- Уменьшено дублирование: логика тестирования определяется один раз и выполняется с несколькими параметрами
- Удобство сопровождения: изменения в логике тестирования применяются ко всем комбинациям параметров
- Улучшена читаемость: четкое разделение между структурой теста и тестовыми данными
- Масштабируемость: легко добавлять новые тестовые примеры, добавляя строки параметров
При этом каждый тест запустится со своими параметрами как отдельный step:
Step 1, params: ('/', 200)
Step 2, params: ('/not_found', 404)
Чего пока нет, но будет в релизе
Lifecycle-хуки before / after (ваше предложение!). Это позволит автоматически поднимать и останавливать сервисы прямо из тестового сценария. Пример того, как это будет выглядеть:
name: My API tests
base_url: http://localhost:3000
before:
command: "docker-compose up -d"
wait_for: "http://localhost:3000/health" # опциональная проверка готовности
after:
command: "docker-compose down"
steps:
- name: test endpoint
request:
method: GET
url: /api
Валидация данных
name: Пример расширенной проверки
steps:
- name: получение данных пользователя
request:
method: GET
url: /api/users/123
expect:
status: 200
validate:
- compare: { path: "user.age", gt: 18 }
- compare: { path: "user.name", min_length: 3 }
- compare: { path: "user.email", regex: ".+@.+\\..+" }
- compare: { path: "items", type: "array", not_empty: true }
Почему это не очередной велосипед?
Да, существуют Postman, Insomnia, Swagger UI, REST-assured, pytest + requests. Но у каждого из этих инструментов есть своя «ахиллесова пята»:
-
Postman — хорош для ручного запуска, но его коллекции плохо версионировать и сложно встраивать в CI (Newman — это ещё одна прослойка).
-
pytest + requests — требует написания кода, а при росте числа тестов страдает поддерживаемость.
-
Karate (Java DSL) — мощный, но всё равно требует знания Java-экосистемы.
YATL же предлагает универсальный язык описания тестов, который понятен всем в команде. Аналитик может добавить проверку нового эндпоинта, не дёргая разработчика. QA-инженер — переиспользовать переменные и схемы. А разработчик — быстро отладить падающий тест, просто взглянув на YAML.
Установить и попробовать библиотеку уже можно через тестовый пакет yatl-testing устанавливается командой pip install yatl-testing.
Кстати, поставить звёздочку на GitHub – это лучший способ сказать нам «спасибо»: ссылка на репозиторий