Перейти к содержимому

API-тестирование

tapstep не заменяет Postman — он делает API первоклассным участником внутри флоу. Шаги request: служат для подготовки, очистки и проверок бэкенда и свободно чередуются с UI-шагами в одном файле: что создали запросом — то проверили на экране, с общими ${переменными}.

Флоу с device: none не резолвит и не бутит устройство — запускается где угодно и мгновенно:

device: none
env: { API: "https://api.example.dev" }
commands:
- request:
method: POST
url: ${API}/staff
auth: { bearer: "${STAFF_TOKEN}" }
json: { email: "qa@test.dev", role: staff }
extract: { staffId: "$.id" } # JSONPath → ${staffId}
assert:
status: 201 # код или класс: "2xx"
json: { "$.role": staff }
- request:
url: ${API}/staff/${staffId}
retryUntil: { status: "2xx" } # поллинг раз в секунду до успеха
timeout: 15000

Под device: none разрешены только request, sql, скрипты (evalScript, runScript), value-ассерты (assertTrue) и control flow (runFlow, retry, repeat). Инструмент validate_flow MCP-сервера (и write_test чата) ловит UI-команду заранее; у tapstep test такого линта нет, там шаг упадёт в рантайме с this flow runs with device: none — UI commands have no device. В десктопе такие флоу запускаются без выбора устройства; в CLI устройство не ищется и не бутится — если только не передать --driver или --browser: они перекрывают device: none и гоняют флоу на этой цели.

В обычном (девайсном) флоу request: свободно смешивается с UI-шагами:

appId: https://staging.example.dev
commands:
- request: # arrange: создать через API
method: POST
url: ${API}/staff
auth: { bearer: "${STAFF_TOKEN}" }
json: { email: "qa@test.dev" }
extract: { staffId: "$.id" }
- openLink: "${APP}/staff/${staffId}" # act + assert: проверить в UI
- assertVisible: "qa@test.dev"

http: — алиас request:, поля те же.

Поле Смысл
method, url GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS; GET по умолчанию. - request: <url> — шортхенд для GET.
headers, query Строковые map-ы, с ${}-подстановкой.
json / body Структурный JSON или сырая строка — одно из двух, никогда оба.
auth { bearer: "${TOKEN}" } или { basic: { user, pass } }.
extract var: $.json.path — значения ответа в ${var} для следующих шагов. Шаг падает, если тело не JSON или путь ничего не нашёл.
assert status (код или "2xx"), равенства json или { contains: "…" }, headers, schema (файл JSON Schema рядом с флоу или инлайн).
retryUntil Той же формы, что assert, но поллит раз в секунду до успеха или timeout — ожидание состояния бэкенда. assert и retryUntil взаимоисключающие.
timeout мс на запрос (по умолчанию 30000); с retryUntil — общий дедлайн поллинга.

${} подставляется в url, query, headers, auth, json и body — но не в ожидаемые значения под assert / retryUntil, они сравниваются буквально.

Внутри { … } берите в кавычки каждый JSONPath и ${…} — extract: { code: "$.items[0].code" }, headers: { X-Id: "${ID}" }. Скобка без кавычек закрывает мапу, и флоу не разбирается.

После каждого запроса ${response.status} и ${response.body} доступны следующим шагам. При упавшей проверке в отчёт попадают метод, URL, статус и срез тела ответа — одинаково в JUnit, Allure и HTML.

Флоу, который ходит в один API, держит базовый URL, заголовки и сессию в одном месте — в блоке http: шапки или в таком же блоке config.yaml проекта — ближайшего вверх от файла флоу, того же, откуда берутся env: и хуки (блок флоу лежит поверх, ключ за ключом):

device: none
http:
baseUrl: ${API} # к нему дописывается относительный url
headers:
X-Sandbox: "${SANDBOX}"
Authorization: "Bearer ${token}" # не уходит, пока ${token} не задан
cookies: true # по умолчанию: Set-Cookie переносится
commands:
- request:
method: POST
url: /login
json: { email: "${EMAIL}", password: "${PASSWORD}" }
extract: { token: "$.token" }
- request: /me # уходят Bearer <token> и cookie сессии
- request:
url: /issues
headers: { X-Sandbox: other } # свой заголовок шага главнее
  • baseUrl — url запроса без схемы (/me, issues?status=done) дописывается к нему; абсолютный url его не использует.
  • headers — уходят с каждым запросом. В значениях работает ${}, и подстановка делается на каждом шаге, так что токен, извлечённый посреди флоу, есть во всех запросах после него; заголовок, чьи переменные ещё не заданы, не отправляется. Заголовок шага с тем же именем (регистр не важен) главнее, а auth: шага заменяет Authorization по умолчанию.
  • cookies — cookie-jar флоу: каждый Set-Cookie, полученный запросом, уходит в следующих запросах так же, как его отправил бы браузер: на тот же хост (Domain= покрывает поддомены; чужой для хоста домен отклоняется), только под своим Path, никогда по голому http, если cookie Secure (кроме localhost), и не после того, как истёк Expires/Max-Age (Max-Age=0, Expires в прошлом или пустое значение удаляет cookie). false выключает.

У хелпера, подключённого через runFlow: login.helper.yaml, может быть свой блок http:. Он лежит поверх блока флоу только на шагах хелпера — хелпер входа может ходить на сервер авторизации, пока флоу ходит в API, — а со следующего шага снова действуют настройки флоу. Хелпер без своего блока берёт блок флоу.

Запрос, который у вас уже есть строкой curl, перенабирать не нужно:

Terminal window
pbpaste | tapstep convert --from curl # напечатать шаг request
tapstep convert --from curl req.txt -o flows/api.flow.yaml --env
tapstep convert --to curl flows/api.flow.yaml # все шаги request обратно в curl
tapstep convert --to curl flows/api.flow.yaml --step 2 # только второй

Шаг возвращается с assert: { status: 2xx }, если не передан --no-assert, а то, что похоже на секрет — bearer-токен, пароль, — выносится в ${VAR}; --env дописывает имена и значения в .env рядом со сценарием (имя, которое там уже есть, остаётся нетронутым), так что сам сценарий можно коммитить. -o пишет не шаг, а целый флоу с device: none. URL под http.baseUrl проекта записывается относительным, а --to curl дописывает относительный url к базе обратно и выводит заголовки по умолчанию. Флаги, которые есть у curl и которых нет у шага request, попадают в отчёт, а не теряются молча. Те же две конверсии — это MCP-инструменты convert_curl и request_to_curl, а в десктопе импорт есть в редакторе теста.

Шаги sql: выполняют один запрос к Postgres, MySQL или SQLite — та же схема arrange / act / assert, но слоем ниже. Подготовьте фикстуру до того, как UI её тронет, проверьте, что UI на самом деле записал, и уберите за собой:

appId: https://staging.example.dev
db: ${DATABASE_URL} # значение по умолчанию для всех sql-шагов
before:
- sql: # arrange: фикстура прямо в БД
exec: "insert into coupons (code, percent) values ($1, $2)"
params: ["QA10", 10] # $1, $2 в Postgres; ? в MySQL/SQLite
assert: { affected: 1 }
commands:
- launchApp: https://staging.example.dev/checkout
- inputText: "QA10"
- tapOn: "Apply"
- sql: # assert: что записал UI
query: "select status, total from orders where coupon = $1"
params: ["QA10"]
readOnly: true # откатывается — не может изменить данные
extract: { total: "$[0].total" }
assert:
rows: 1
first: { status: paid }
after:
- sql: { exec: "delete from coupons where code = $1", params: ["QA10"] }

before: и after: — подготовка и уборка флоу: after выполняется после последнего шага что бы ни случилось, поэтому уборка выше удалит купон и после упавшей проверки (onFlowStart / onFlowComplete — старые имена этих двух ключей, они всё ещё работают). См. Файл флоу.

assert принимает rows (точное число), minRows, none: true, first (колонки первой строки), contains (какая-то строка совпала по всем колонкам) и affected (только для exec). extract выполняет JSONPath по результату — массив строк для query, { "affected": N } для exec; после шага следующим доступны ${sql.rows} / ${sql.affected}.

Значения всегда идут через params и никогда не попадают в текст SQL, так что кавычки и инъекции — забота драйвера, а не ваша. Параметр ${var} несёт извлечённое значение и связывается как текст, поэтому в Postgres приводите плейсхолдер к типу колонки (where id = $1::int). Запись разрешена сознательно: фикстурам она нужна, а ограничителем служит учётная запись подключения — в сюитах, которые только проверяют, укажите в db: пользователя базы только на чтение. DSN считается секретом: в отчётах, событиях и логах он выглядит как scheme://user:***@host/db — и когда написан прямо во флоу, и когда пришёл из ${DATABASE_URL}.

В десктопе у шага request: и sql: есть кнопка Попробовать — и в строке шага, и в его форме. Она выполняет ровно этот шаг: переменные разрешаются так же, как их разрешил бы прогон, — включая env: и значения params: по умолчанию из шапки самого флоу, даже если шапку правили и ещё не сохранили, — для окружения, выбранного в шапке, а сам файл флоу не меняется. ${var}, который извлёк бы предыдущий шаг, здесь ещё не существует: попытка знает переменные проекта, а не прогона.

Попытка по-настоящему отправляет запрос и по-настоящему выполняет запрос к базе: POST или sql: exec: меняет ту систему, на которую указывает окружение, ровно как в прогоне.

Результат открывается под шагом: статус и время, заголовки ответа, тело и каждый ключ assert:/extract: с тем, что он увидел. У шага sql: строки показаны таблицей, а над ней — их число. Одновременно идёт одна попытка на шаг; закрытие панели её останавливает.

Из ответа же пишутся и проверки. Нажмите на значение в теле — панель предложит два действия:

  • В переменную — добавит extract: { <имя>: "$.path" }; имя предлагается по ключу, на который вы нажали. Путь — тот, который вычисляет движок, в кавычках, если ключ не обычное слово ($.user['user-name']).
  • Проверять, что равно — добавит assert: { json: { "$.path": <значение> } }, причём тип значения тот же, что в ответе: число остаётся числом, строка — строкой.

Нажатие на статус добавляет assert: { status: 200 }. В результате sql: нажатие на ячейку первой строки добавляет assert: { first: { <колонка>: <значение> } } или extract от $[0].<колонка>, а нажатие на число строк — assert: { rows: N } (после exec — assert: { affected: N }). Каждое нажатие пишет один ключ этого шага; больше в файле ничто не сдвигается.

db у шага sql: — выпадающий список переменных текущего окружения, значение которых похоже на строку подключения; они показаны как ${NAME}, сама строка подключения не появляется на экране и не попадает в YAML. Рядом кнопка Проверить: подключается, спрашивает у сервера версию и закрывает соединение, а если не может — называет стадию, на которой не вышло: DNS, connect, auth. В терминале та же проверка — tapstep sql --ping "${DATABASE_URL}".

Сознательно за рамками: нагрузка, моки/стабы, GUI-коллекции. Для E2E-работы привычные ходы Postman — вызвать, извлечь, проверить, подождать — доступны прямо во флоу.