SSD Nodes Learn 🎉 VPS от $5.50/мес
Руководства Matt ConnorАвтор: Matt Connor

Self-hosted API mocking и тестирование на VPS

Разверните WireMock для стабов и Hurl для тестирования API на собственном VPS. Узнайте, как разделить задачи мокинга и проверки эндпоинтов, сохранив отчеты после перезапуска.

Две задачи, использующие один репозиторий

Self-hosted API mocking и тестирование — это две разные задачи, и попытка объединить их приводит к потере недели времени. Mock-сервер подменяет собой зависимость, которую невозможно вызвать из CI: платежный шлюз, API партнера, upstream с лимитами по частоте запросов или сервис, который другая команда еще не выпустила. API test runner вызывает ваши собственные эндпоинты в заданном порядке и проверяет ответы, передавая значения из одного ответа в следующий запрос.

Эти задачи не пересекаются. Mock-сервер никогда не сообщает о прохождении или провале тестов. Test runner не оценивает, что именно возвращает платежный провайдер при отклонении карты. Большинство команд, арендующих сервер, в итоге запускают оба инструмента, используя один и тот же файл Docker Compose и проводя ревью в рамках одного pull request.

Почему стоит использовать self-hosted решения для мокинга и тестирования API?

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

Вторая причина — доступность. Сервис, привязанный к частному адресу, недоступен из облачного раннера, поэтому тест не может быть запущен. Любое обходное решение требует затрат. Публикация API в Интернете ради тестирования лишает смысла его нахождение в закрытой сети. Туннель или публичная копия для staging — это еще одна система, которую нужно обслуживать, причем staging-копия со временем начинает отличаться от production между релизами. Раннер в той же частной сети обращается к сервису напрямую и не требует подобных ухищрений, что является практическим аргументом в пользу использования self-hosted GitHub Actions runner.

Какой self-hosted mock-сервер выбрать?

Каждый из них запускается в виде контейнера на вашем собственном сервере. Важный вопрос заключается в том, что именно считается источником истины (source of truth), так как от этого зависит, потребует ли пересборка контейнера нулевых усилий или целого дня работы.

  • WireMock хранит каждый стаб в виде JSON-файла в директории mappings/, а большие тела ответов — в __files/. Образ называется wiremock/wiremock, его корневая директория внутри контейнера — /home/wiremock, также он может работать как записывающий прокси. Хранение файлов на диске означает, что мок-сервер можно версионировать в git, как и любой другой код.
  • Mockoon CLI хранит весь mock API в одном JSON-файле данных. Установите его с помощью npm install -g @mockoon/cli и запустите командой mockoon-cli start --data ./data-file.json, либо используйте образ mockoon/cli с примонтированным файлом. Десктопное приложение редактирует тот же самый файл, поэтому проектирование через UI и последующий коммит результатов полностью совместимы.
  • MockServer запускается из образа mockserver/mockserver и слушает порт 1080. Ожидания (expectations) передаются через его собственный REST API, что удобно для тестового кода, но рискованно для продакшена: ожидание, созданное HTTP-запросом, исчезает при перезапуске контейнера. Используйте его JSON-файл инициализации для стабов, которые должны быть постоянными.
  • Prism создает мок на основе вашего OpenAPI-документа, а не отдельных файлов стабов. Установите его через npm install -g @stoplight/prism-cli, затем запустите prism mock openapi.yaml. Внутри контейнера добавьте флаг -h 0.0.0.0, так как по умолчанию Prism привязывается к localhost и иначе будет недоступен извне контейнера.
  • Microcks — это масштабное решение: веб-интерфейс, который импортирует OpenAPI-документы и коллекции Postman, после чего предоставляет их в качестве моков и запускает контрактные тесты. Для полной установки требуются MongoDB и Keycloak, а также Kafka для асинхронных функций. Образ «всё в одном» microcks-uber включает встроенную in-memory MongoDB, которую разработчики рекомендуют использовать только для временных задач. Поэтому считайте всё, что создано в этом UI, одноразовым, а исходные артефакты храните в git.

Какой инструмент для тестирования API с самостоятельным хостингом выбрать?

Задача состоит из последовательности действий: аутентификация, создание заказа, чтение данных и проверка изменения состояния. Для этого необходимо извлечь значение из одного ответа и использовать его в следующем запросе. Инструмент, который не умеет передавать состояние между вызовами, является средством проверки работоспособности (health check), а не инструментом для тестирования API.

  • Hurl выполняет HTTP-запросы из обычных текстовых файлов с помощью одного бинарного файла. Секция [Captures] извлекает значения из ответа, секция [Asserts] проверяет их, а --test превращает инструмент в полноценный тест-раннер с выводом сводки и кодом завершения. Версия 8.0.1 является актуальной на август 2026 года.
  • Bruno CLI запускает папку с файлами .bru. Установите его с помощью npm install -g @usebruno/cli, затем выполните bru run folder --env Local --reporter-junit results.xml. Формат коллекции — это текстовые файлы в директории, поэтому изменения (diff) легко читаются при проверке кода.
  • Newman запускает коллекции Postman вне интерфейса Postman: npm install -g newman, затем newman run collection.json -r cli,junit --reporter-junit-export results.xml. Нюанс заключается в формате. Коллекция представляет собой один экспортированный JSON-файл, поэтому редактирование происходит в Postman, а файл в git является копией, которая быстро устаревает.
  • Schemathesis — это другой тип проверки. Он считывает схему OpenAPI и генерирует тестовые случаи, которые пытаются получить ответы, невозможные согласно вашей схеме: uvx schemathesis run https://your.api/openapi.json. Он находит сбои и нарушения контракта, но ничего не знает о вашей бизнес-логике, поэтому он дополняет скриптовый набор тестов, а не заменяет его.
  • Hoppscotch с самостоятельным хостингом — это вариант с веб-интерфейсом, требующий наличия экземпляра Postgres. Учитывайте этот компромисс перед установкой: коллекции хранятся в базе данных, а не в вашем репозитории.

Чего стоит избегать. Step CI всё ещё встречается в подборках инструментов, и его формат рабочих процессов на YAML выглядит удобно, но последний коммит в репозиторий был сделан в августе 2024 года. Программа, которая находится между вашей CI-системой и API, — это неудачное место для заброшенного кода.

Размещение mock-сервера за межсетевым экраном

Приведенная ниже конфигурация запускает WireMock в качестве имитации платежного шлюза. Если формат compose-файла для вас в новинку, в Docker Compose на VPS описаны команды жизненного цикла, которые подразумеваются в этом разделе.

services:
  mock-payments:
    image: wiremock/wiremock:3.13.2
    command: ["--verbose"]
    volumes:
      - ./mocks/payments:/home/wiremock
    ports:
      - "127.0.0.1:8080:8080"
    restart: unless-stopped

Префикс 127.0.0.1: перед портом — это важная деталь. Обычная запись 8080:8080 публикует mock-сервер на всех интерфейсах, включая ваш публичный IP-адрес. Он останется доступным, даже если ufw блокирует этот порт, так как Docker записывает свои правила в цепочку iptables DOCKER, а они обрабатываются раньше правил INPUT в ufw. Привяжите сервис к адресу loopback или к адресу частного интерфейса, и ядро не будет принимать внешние соединения.

Ваш тестируемый сервис должен обращаться к mock-серверу. Если сервис запущен в том же проекте compose, базовый URL mock-сервера будет http://mock-payments:8080, так как compose разрешает имена сервисов в своей сети. Если сервис запущен на хосте, используйте http://127.0.0.1:8080. Указывайте этот адрес через переменную окружения, а не в коде, иначе тестовый URL попадет в production.

Заглушки (stubs) размещаются в ./mocks/payments/mappings/, по одному JSON-файлу на каждую.

{
  "request": {
    "method": "POST",
    "urlPath": "/v1/charges",
    "bodyPatterns": [{ "matchesJsonPath": "$.amount" }]
  },
  "response": {
    "status": 201,
    "headers": { "Content-Type": "application/json" },
    "jsonBody": { "id": "ch_test_001", "status": "succeeded", "amount": 4200 }
  }
}

Запустите сервис, а затем проверьте, что именно было загружено.

docker compose up -d --wait mock-payments
curl -fsS http://127.0.0.1:8080/__admin/mappings

Команда --wait блокирует выполнение до тех пор, пока контейнер не сообщит о состоянии healthy. Это работает, потому что образ WireMock содержит HEALTHCHECK для проверки эндпоинта /__admin/health. Вызов mappings выводит список всех заглушек, которые считал сервер. Если созданная вами заглушка отсутствует в этом списке, значит, она не была загружена: проверьте, что файл находится в mappings/, а не в корневой директории монтирования, и убедитесь в корректности JSON.

Когда поступает запрос, для которого нет подходящей заглушки, WireMock отвечает 404 с телом, начинающимся с Request was not matched, за которым следует diff с наиболее похожей заглушкой. Прочитайте этот diff перед внесением изменений, так как в нем указано конкретное поле, вызвавшее расхождение. Обычно это путь с /v1/charge, в то время как в заглушке указано /v1/charges.

Написание теста в виде последовательности с сохранением состояния между вызовами

Файлы Hurl представляют собой обычный текст. Установите deb-пакет из релизов проекта.

VERSION=8.0.1
curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/$VERSION/hurl_${VERSION}_amd64.deb
sudo apt update && sudo apt install ./hurl_${VERSION}_amd64.deb

Набор тестов, проверяющий ваш API с использованием заглушки (mock), находится в tests/checkout.hurl.

POST {{base_url}}/orders
Content-Type: application/json
{
  "sku": "ssd-1tb",
  "amount": 4200
}
HTTP 201
[Captures]
order_id: jsonpath "$['id']"

GET {{base_url}}/orders/{{order_id}}
HTTP 200
[Asserts]
jsonpath "$.status" == "paid"
jsonpath "$.charge_id" == "ch_test_001"

Блок [Captures] превращает это в полноценный API-тест, а не в два независимых запроса. Значение order_id считывается из первого ответа и подставляется в URL второго запроса. Проверка в charge_id — это ключевой момент всей процедуры: она подтверждает, что ваш сервис обратился к платёжному провайдеру и сохранил полученные данные, а сравниваемое значение совпадает с тем, которое вы указали в WireMock stub. Теперь один файл охватывает обе части процесса.

hurl --test --variable base_url=http://127.0.0.1:3000 \
  --report-junit reports/junit.xml \
  --report-json reports/json \
  tests/

При успешном выполнении выводится по одной строке на каждый файл и итоговая сводка.

tests/checkout.hurl: Success (2 request(s) in 61 ms)
Executed files:    1
Executed requests: 2 (30.1/s)
Succeeded files:   1 (100.0%)
Failed files:      0 (0.0%)
Duration:          64 ms

В случае ошибки выводится error: Assert failure с указанием файла и номера строки, затем полученное значение в сравнении с ожидаемым, после чего hurl завершается с ненулевым кодом, чтобы остановить CI. Если status считывает pending там, где вы ожидали paid, значит, ваш сервис не обработал ответ от заглушки. Следующим шагом следует изучить журнал запросов WireMock по адресу /__admin/requests, который покажет, дошёл ли вызов до заглушки в принципе.

Запуск набора тестов с собственного CI-раннера

Если раннер зарегистрирован на том же сервере, рабочий процесс упрощается. Раннер работает как обычный процесс в системе, поэтому docker и hurl должны быть установлены на этом хосте. Никакие компоненты из облачных образов не наследуются.

name: api-tests
on: [push]
jobs:
  hurl:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - name: Start the mock
        run: docker compose up -d --wait mock-payments
      - name: Run the suite
        run: hurl --test --variable base_url=http://127.0.0.1:3000 --report-junit reports/junit.xml tests/
      - name: Archive the reports
        if: always()
        run: install -d /srv/api-tests/reports/$GITHUB_SHA && cp -r reports/. /srv/api-tests/reports/$GITHUB_SHA/
      - name: Stop the mock
        if: always()
        run: docker compose down

Параметр if: always() на этапе архивации имеет решающее значение. Без него при сбое тестов копирование не выполняется, и вы теряете отчет, который необходимо изучить. Копирование должно происходить за пределами рабочей директории, так как раннер очищает её перед выполнением следующего задания, удаляя при этом все отчеты.

Сохраняйте результаты, а не только последний запуск

JUnit XML-файл для каждого коммита отвечает на один вопрос: прошел ли тест. Он не дает ответа на вопрос, когда именно эндпоинт начал работать медленнее, так как после закрытия файлов их никто не просматривает. Чтобы отслеживать динамику, добавляйте по одной строке за каждый запуск в небольшую базу данных на том же сервере. Достаточно одной таблицы, содержащей SHA коммита, имя файла, количество успешных и провальных тестов, а также длительность выполнения. SQLite в продакшене на VPS — подходящее решение для этой задачи: один файл, отсутствие серверного процесса, а вся история попадает в резервные копии, которые вы уже делаете. Парсите вывод --report-json утилиты Hurl, а не JUnit XML, так как из этих двух форматов именно он предназначен для машинного чтения.

Что должно сохраняться при пересборке контейнера

Определения заглушек (mock) и наборы тестов — это исходный код. Они должны находиться в репозитории рядом с сервисом, который они описывают, и изменяться в том же pull request, что и сам endpoint. Заглушка, отредактированная через веб-интерфейс, или ожидание, отправленное в MockServer через его REST API во время выполнения, существуют только в оперативной памяти контейнера или базе данных этого инструмента. Выполните docker compose down, и они исчезнут, причем никто не заметит этого, пока тест не начнет проходить по неверной причине. Если ваши репозитории также размещены на вашем собственном оборудовании, собственный git-сервер позволит хранить фикстуры и сервис в рамках одного контура доверия.

Теперь практические правила. Фиксируйте теги образов, так как latest может изменить способ сопоставления запросов вашей заглушкой без каких-либо изменений в вашем репозитории, и такую ошибку крайне сложно связать с первопричиной. Монтируйте каталоги с заглушками в режиме только для чтения, если инструменту не требуется запись в них. Никогда не помещайте заглушки в именованный Docker volume, так как в этом случае том становится единственным источником истины, а копия в git незаметно устаревает.

И еще один момент, на котором часто попадаются. Если вы создаете заглушки путем записи реального трафика через прокси, просматривайте каждый сгенерированный файл перед коммитом. Запись содержит в точности то, что прислал upstream, включая bearer tokens и адреса электронной почты клиентов. Коммит такого файла навсегда помещает эти данные в ваш репозиторий, так как git сохраняет удаленное содержимое в истории.

FAQ

В чем разница между mock-сервером API и инструментом для запуска тестов API?

Mock-сервер отвечает на запросы. Он заменяет зависимость, к которой нельзя обратиться из CI, и никогда не сообщает о прохождении или провале теста. Инструмент для запуска тестов API отправляет запросы к вашему сервису, проверяет ответы, передает значения из одного вызова в другой и завершается с ненулевым кодом при ошибке проверки. Они решают разные задачи, и в типичной конфигурации используются оба: инструмент вызывает ваш сервис, а ваш сервис вызывает mock.

Можно ли тестировать внутренний API с помощью облачного CI-раннера?

Только если открыть к нему доступ. Облачный раннер находится вне вашей сети, поэтому он не может обратиться к сервису, привязанному к частному адресу. У вас есть варианты: опубликовать API, запустить туннель или поддерживать публичную копию для тестирования, но каждый из них добавляет систему, которая может выйти из строя или допустить утечку данных. Раннер в той же частной сети обращается к сервису напрямую, что является основной практической причиной, по которой команды предпочитают размещать такие системы на собственных серверах.

Где должны храниться заглушки mock-сервера и наборы тестов API?

В git, рядом с сервисом, который они описывают. Инструменты, хранящие определения в виде файлов, такие как каталог mappings/ в WireMock, файл данных Mockoon, файлы Hurl и папка .bru в Bruno, позволяют проводить проверку кода и пересборку контейнера без дополнительных затрат. Инструменты, хранящие определения в базе данных или веб-интерфейсе, требуют плана резервного копирования и этапа экспорта, о котором часто забывают до тех пор, пока контейнер не будет удален.

Почему мой mock возвращает 404, хотя заглушка выглядит правильно?

WireMock возвращает заглушку только при точном совпадении. На запрос, для которого не найдено соответствие, возвращается 404 с телом, начинающимся с Request was not matched, за которым следует сравнение с наиболее похожей заглушкой, где указано поле, вызвавшее расхождение. Частые причины: лишний слэш в пути, заголовок Content-Type, который требует заглушка, но не отправил клиент, использование urlPath там, где заглушке нужно urlPathPattern для переменного сегмента, или несоответствие матчера тела полезной нагрузке. Сначала проверьте /__admin/requests, чтобы убедиться, что запрос вообще дошел до mock-сервера.

Нужны ли мне mock-серверы, если есть staging-окружение?

Да, по двум причинам. Копия внешнего сервиса на staging-окружении, который вы не контролируете, может выйти из строя или ограничить количество запросов, из-за чего ваш набор тестов упадет по причинам, не связанным с вашим кодом. Кроме того, staging не может сгенерировать ответы, которые критически важны для тестирования, например, отказ по карте или таймаут шлюза. Mock-сервер возвращает их по требованию на скорости локальной сети, что сокращает время выполнения набора тестов с минут до секунд. Оставьте staging для финальной проверки перед релизом, а в CI используйте mock-серверы.