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 – это лучший способ сказать нам «спасибо»: ссылка на репозиторий