VPSでWireMockとHurlをセルフホストする方法
WireMockはGitで管理するスタブ、HurlはCIで実行するAPIテストです。VPS上でDocker Composeにまとめ、再構築後もレポートを残す構成を紹介します。
1 つのリポジトリを共有する 2 つのジョブ
セルフホスト型の API モックとテストは別のジョブです。これらを 1 つにまとめると、1 週間を無駄にします。モックサーバーは、CI から呼び出せない依存先の代わりになります。決済プロバイダー、パートナー API、レート制限のある上流サービス、または別チームがまだリリースしていないサービスなどです。API テストランナーは、自分たちのエンドポイントを決められた順序で呼び出し、レスポンスを検証します。あるレスポンスの値を次のリクエストに引き継ぐこともできます。
この 2 つの役割は重なりません。モックサーバーがテストの成功や失敗を報告することはありません。テストランナーも、カードが拒否されたときに決済プロバイダーが返す内容を決めるものではありません。すでにサーバーを借りているチームの多くは、同じ Docker Compose ファイルで起動し、同じ pull request でレビューするモックサーバーとテストランナーを 1 つずつ運用します。
API のモックとテストをセルフホストする理由
フィクスチャは本番環境に近いデータです。API テストのリクエストボディには、名前だけを変更した実際の顧客レコードが含まれることがあります。誰も確認していなければ、名前すら変更されていない場合もあります。記録したスタブはさらに危険です。プロキシによる記録では、上流サービスが実際に返した内容が保存されます。そのため、記録から作成したスタブディレクトリには、すべてのファイルを確認するまで、実際のトークンや顧客のメールアドレスが残ります。ホスト型サービスでは、そのデータが他者のインシデントとなり、情報開示につながります。
2 つ目の理由は到達性です。プライベートアドレスにバインドされたサービスには、ホスト型 runner からアクセスできないため、テスト自体を実行できません。どの回避策にもコストがかかります。テストのために API をインターネットへ公開すると、非公開にしていた理由が失われます。トンネルやパブリックな staging コピーを使う場合は、別のシステムを保守する必要があります。また、staging コピーはリリースの間に本番環境との差異が生じます。同じプライベートネットワーク上の runner なら、サービスを直接呼び出せるため、これらは不要です。これが、セルフホストの GitHub Actions runnerを使う実際的な理由です。
実行すべきセルフホスト型モックサーバーはどれですか?
これらはすべて、所有するサーバー上でコンテナとして実行できます。重要なのは、それぞれが何を正本として扱うかです。これにより、コンテナを再構築したときに何も失わずに済むか、復旧に半日かかるかが決まります。
- WireMock は、すべてのスタブを
mappings/ディレクトリ内の JSON ファイルとして保持し、大きなレスポンス本文を__files/に保存します。イメージはwiremock/wiremock、コンテナ内の root ディレクトリは/home/wiremockです。記録プロキシとしても実行できます。ディスク上のファイルを使うため、他のコードと同じようにモックを git で管理できます。 - Mockoon CLI は、モック API 全体を 1 つの JSON データファイルに保持します。
npm install -g @mockoon/cliでインストールしてmockoon-cli start --data ./data-file.jsonで起動するか、そのファイルを bind mount してmockoon/cliイメージを実行します。デスクトップアプリも同じファイルを編集するため、UI で設計した結果をコミットする運用と互換性があります。 - MockServer は
mockserver/mockserverイメージから実行し、1080 番ポートで待ち受けます。期待値は専用の REST API 経由で登録します。テストコードから扱うには便利ですが、デプロイ方法としては注意が必要です。HTTP リクエストで作成した期待値は、コンテナを再起動すると消えます。永続化するスタブには、JSON 初期化ファイルを使用してください。 - Prism は、個別のスタブファイルではなく OpenAPI ドキュメントからモックを構築します。
npm install -g @stoplight/prism-cliでインストールして、prism mock openapi.yamlを実行します。コンテナ内では-h 0.0.0.0を追加してください。Prism はデフォルトで localhost に bind するため、設定しないとコンテナの外部から接続できません。 - Microcks は大規模な選択肢です。Web UI で OpenAPI ドキュメントや Postman コレクションをインポートし、それらをモックとして提供し、コントラクトテストも実行します。完全なインストールには MongoDB と Keycloak が必要で、非同期機能には Kafka も必要です。オールインワンの
microcks-uberイメージにはメモリ内 MongoDB が含まれています。プロジェクトでは一時的な用途に適すると説明しているため、UI で作成したものは使い捨てとして扱い、元のアーティファクトは git に保持してください。
運用すべき self-hosted API テストランナーはどれですか?
ここで必要なのは、認証、注文の作成、注文の再取得、状態が変わったことのアサートという一連の処理です。あるレスポンスから取得した値を、次のリクエストで使う必要があります。呼び出し間で状態を引き継げないツールは、API テストではなくヘルスチェックです。
- Hurl は、単一のバイナリでプレーンテキスト形式の HTTP リクエストファイルを実行します。
[Captures]セクションでレスポンスから値を取り出し、[Asserts]セクションで値を検証し、--testによってサマリーと終了コードを備えたテストランナーにできます。2026 年 8 月時点の最新バージョンは 8.0.1 です。 - Bruno CLI は
.bruファイルのフォルダーを実行します。npm install -g @usebruno/cliでインストールし、bru run folder --env Local --reporter-junit results.xmlで実行します。コレクション形式は設計上、ディレクトリ内のテキストファイルです。そのため、レビュー時に差分を読みやすく確認できます。 - Newman は Postman の外部で Postman コレクションを実行します。
npm install -g newmanを実行し、その後newman run collection.json -r cli,junit --reporter-junit-export results.xmlを実行します。注意点は形式です。コレクションは 1 つのエクスポート済み JSON データになるため、編集は Postman で行います。git に保存したファイルはコピーなので、次第に実態とずれていきます。 - Schemathesis は別種のチェックツールです。OpenAPI スキーマを読み取り、スキーマ上は発生しないはずのレスポンスを生成するケースを作成します。
uvx schemathesis run https://your.api/openapi.json。クラッシュやコントラクト違反を検出できますが、ビジネスルールは認識しません。そのため、スクリプト化したテストスイートの代わりではなく、併用するツールです。 - Hoppscotch self hosted は Web UI を使う選択肢で、Postgres インスタンスが必要です。インストールする前に、このトレードオフを理解してください。コレクションはリポジトリではなくデータベースに保存されます。
避けるべきツールもあります。Step CI はツールの比較記事に現在も登場し、YAML ワークフロー形式も読みやすいものです。しかし、リポジトリへの最後のコミットは 2024 年 8 月です。CI と API の間に置くプログラムに、保守されていないコードを使うのは適切ではありません。
ファイアウォールの内側にモックサーバーを配置する
以下の構成では、支払いプロバイダーの代替として WireMock を実行します。Compose ファイルの形式に慣れていない場合は、VPS 上の Docker Composeで、このセクションで前提とするライフサイクルコマンドを説明しています。
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 では、パブリック IP を含むすべてのインターフェースでモックを公開します。また、ufw がそのポートを拒否していても到達可能なままです。これは、Docker が独自のルールを DOCKER iptables チェーンに書き込み、それらが ufw の INPUT ルールより先に評価されるためです。代わりにループバックアドレス、またはプライベートインターフェースのアドレスにバインドすると、カーネルは外部からの接続を受け付けません。
次に、テスト対象のサービスからモックを指定します。サービスが同じ Compose プロジェクトで実行される場合、Compose が独自のネットワーク上でサービス名を解決するため、モックのベース URL は http://mock-payments:8080 です。サービスがホスト上で実行される場合は http://127.0.0.1:8080 です。これらは環境変数で設定し、コードには記述しないでください。そうしないと、テスト用 URL が本番環境にリリースされます。
スタブは ./mocks/payments/mappings/ に配置します。1 つの JSON ファイルに 1 つのスタブを記述します。
{
"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 イメージに /__admin/health エンドポイントを確認する HEALTHCHECK が含まれているため機能します。mappings コマンドは、サーバーが読み込んだすべてのスタブを一覧表示します。作成したスタブが一覧にない場合、そのスタブは読み込まれていません。ファイルがマウント先のルートではなく mappings/ 配下にあることと、JSON を解析できることを確認してください。
リクエストを受信して一致するスタブがない場合、WireMock は 404 を返します。本文は Request was not matched で始まり、その後に保持している最も近いスタブとの差分が続きます。変更を加える前に、その差分を確認してください。異なる正確なフィールドが示されるためです。通常は、スタブでは /v1/charges となっている /v1/charge を含むパスです。
テストを呼び出し間で状態を引き継ぐシーケンスとして記述する
Hurl ファイルはプレーンテキストです。プロジェクトの releases から 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 をテストするスイートは 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] ブロックにより、これは無関係な2つのリクエストではなく API テストになります。order_id は最初のレスポンスから読み取られ、2つ目の URL に埋め込まれます。charge_id に対するアサーションが、このテストの目的です。これにより、サービスが決済プロバイダーを呼び出し、返された値を保存したことを検証できます。比較対象の値は、WireMock のスタブに記述した値です。これで、フローの両方の処理を1つのファイルで検証できます。
hurl --test --variable base_url=http://127.0.0.1:3000 \
--report-junit reports/junit.xml \
--report-json reports/json \
tests/テストに成功すると、ファイルごとに1行とサマリーが出力されます。
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 が、期待した paid ではなく pending を読み取る場合、サービスはモックのレスポンスを処理できていません。次に確認するのは /__admin/requests の WireMock リクエストジャーナルです。ここで、呼び出しがモックに到達したかどうかを確認できます。
自身の CI runner から suite を実行する
同じホストに runner を登録しておけば、workflow は短くなります。runner はホスト上で動作する通常のプロセスなので、そのホストに docker と hurl をインストールする必要があります。hosted image の内容は引き継がれません。
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() が重要です。これがないと、テスト実行が失敗した場合にコピーがスキップされ、確認したかったレポートを失います。コピー先は workspace の外部にする必要もあります。runner は次の job の前に workspace をクリーンアップするため、レポートも削除されるからです。
結果を保持し、直近の実行結果だけに依存しない
コミットごとに JUnit XML ファイルを保存すると、「成功したか」という 1 つの問いには答えられます。しかし、ファイルを開かなくなった後は誰も読み取らないため、エンドポイントの応答が遅くなり始めた時期は分かりません。傾向を把握するには、同じホスト上の小規模なデータベースに、実行ごとに 1 行を追加します。コミット SHA、ファイル名、成功数、失敗数、実行時間を保持する単一のテーブルで十分です。保存先としては VPS 上の本番環境での SQLite が適しています。1 つのファイルだけで済み、サーバープロセスも不要で、既存のバックアップに履歴全体を含められます。2 つの形式のうち機械可読形式であるため、JUnit XML ではなく Hurl の --report-json 出力を解析してください。
コンテナを再ビルドしても残すべきもの
モックの定義とテストスイートはソースコードです。これらは、対象のサービスと同じリポジトリで管理し、エンドポイントを変更するプルリクエストで同時に変更します。Web UI で編集したスタブや、実行時に REST API 経由で MockServer へ登録した期待値は、そのコンテナのメモリまたはツールのデータベースにしか存在しません。docker compose downを実行すると失われます。そして、テストが誤った理由で成功し始めるまで、誰も気付きません。リポジトリを自前のハードウェアでも運用している場合は、セルフホスト型の git サーバーを使うと、フィクスチャとサービスを同じ信頼境界内に置けます。
次に、実務上のルールを示します。イメージタグは固定してください。latestによって、リポジトリを変更していなくてもモックのリクエスト照合方法が変わる可能性があり、その障害を原因と結び付けるのは非常に困難だからです。ツールによる書き込みが不要な場合は、スタブのディレクトリを読み取り専用でマウントします。モックのスタブを名前付き Docker ボリュームに置いてはいけません。ボリュームが正となり、git にあるコピーが気付かないうちに正しくない状態になるためです。
もう1つ注意点があります。プロキシ経由で実際のトラフィックを記録してスタブを作成する場合は、生成されたファイルをすべて確認してからコミットしてください。記録には、上流サービスが返した内容がそのまま含まれます。ベアラートークンや顧客のメールアドレスも例外ではありません。それをコミットすると、リポジトリに永久に残ります。git は削除した内容も履歴に保持するためです。
FAQ
API mock server と API test runner の違いは何ですか?
Mock server はリクエストに応答します。CI から呼び出せない依存先の代わりになりますが、テストの成功または失敗は報告しません。API test runner は自分のサービスにリクエストを送り、レスポンスをアサートし、ある呼び出しの値を次の呼び出しに引き継ぎ、アサーションが失敗すると non zero で終了します。両者は異なる問題を解決します。一般的な構成では両方を同時に実行し、runner が自分のサービスを呼び出し、そのサービスが mock を呼び出します。
ホスト型 CI runner から内部 API をテストできますか?
公開しない限り、できません。ホスト型 runner はネットワークの外部にあるため、プライベートアドレスにバインドされたサービスへ接続できません。API を公開する、トンネルを実行する、公開された staging 環境を維持する、といった選択肢がありますが、どれも障害や情報漏えいの原因になり得るシステムを追加します。同じプライベートネットワーク上の runner ならサービスを直接呼び出せます。これが、この作業を self host する主な実務上の理由です。
mock stub と API test suite はどこに置くべきですか?
定義対象のサービスの近くに、git で管理してください。WireMock の mappings/ directory、Mockoon の data file、Hurl files、Bruno の .bru folder など、定義をファイルとして保存するツールなら、コードレビューと、コストをかけずに実行できるコンテナの再ビルドが可能です。定義をデータベースや Web UI に保存するツールでは、バックアップ計画と export 手順が必要です。コンテナがすでに消えてから、export を忘れていたことに気付くケースが多くあります。
stub が正しいように見えるのに、mock が 404 を返すのはなぜですか?
WireMock は完全一致した場合にだけ stub を提供します。一致しないリクエストには 404 を返し、本文は Request was not matched で始まります。その後に最も近い stub との差分が続き、異なるフィールドが示されます。よくある原因は、パス末尾のスラッシュ、stub が要求しているのにクライアントが送信していない Content-Type header、可変セグメントに対して stub が urlPathPattern を必要としているのに urlPath を使用していること、payload に合わない body matcher です。まず /__admin/requests を確認し、リクエストが mock に到達していることを確認してください。
staging 環境がある場合でも mock は必要ですか?
はい。理由は2つあります。管理していない upstream の staging copy は停止することがあり、rate limit も適用されるため、コードとは無関係な理由で suite が失敗します。また、declined card や gateway timeout など、テストで特に必要なレスポンスを生成できないこともあります。mock ならそれらを必要なときに local network の速度で返せます。そのため、sandbox に対して数分かかる suite を数秒で実行できます。release 前の最終確認には staging を使い、CI では mock を使用してください。