OpenAPI(旧Swagger)の仕様書をテストの正解として使えば、APIの実装が仕様からずれた時点でCIで止められます。Jestでこれを行う定番は jest-openapi ですが、最終リリースは2022年1月の0.14.2で、OpenAPI 3.1の仕様書も3.0の規則で検査するため、type: [string, "null"] を含む仕様は読み込みの段階でエラーになります。本記事では、Jest 30で実行した結果をもとに、3.0で使う手順、3.1の応答を照合する代替、入力を自動生成するSchemathesis、シナリオを書くStepCIの使い分けを整理します。
まとめ:OpenAPI仕様でAPIテストを自動化するときの選び方
- 仕様が3.0以下なら jest-openapi。
expect(res).toSatisfyApiSpec()の1行で、ステータスとボディを仕様に照合できます。 - 仕様が3.1なら、読み込めたかどうかだけで対応可否を判断しない。jest-openapiは3.1に正式対応しておらず、
typeの配列指定を含む仕様はInvalid OpenAPI specで止まります。3.1の仕様を使うなら、Ajvの2020-12対応版で応答ボディだけを照合する自作マッチャが手早い代替です。 - 入力値を変えて叩き、仕様違反の受け入れや5xxを探すのは Schemathesis。手書きのテストでは思いつかない値を仕様から生成します。
- ログインを挟む複数リクエストの流れは StepCI のYAMLで書けますが、2024-06-10の2.8.2を最後にリリースが無く、既定で利用状況を外部へ送信します。
- どの方法も「仕様書が更新され続けること」が前提です。仕様を直さずに実装だけ変える現場では、テストが無効化されて終わります。
仕様駆動のAPIテストで照合できる3つの対象
OpenAPIを使うテストは、何を照合するかで道具が分かれます。最初にどれが欲しいかを決めると、ツールを重ねて入れる無駄がなくなります。
| 照合する対象 | 見つかる不具合 | 向く道具 |
|---|---|---|
| 応答の形 | 型違い・必須項目の欠落 | jest-openapi/Ajv |
| 不正な入力の拒否 | 不正値を200で受ける・5xx | Schemathesis |
| 複数リクエストの流れ | トークン引き回しの失敗 | StepCI |
仕様書そのものの書き方はOpenAPIの基本とSwaggerとの違い、3.0・3.1・3.2の差分を参照してください。フロントエンドとの並行開発で仕様からモックを立てる用途(Prism等)はテストではなくAPIモッキングとモックサーバーの方式選定の領域です。
jest-openapiでレスポンスを仕様に照合する手順(OpenAPI 3.0まで)
jest-openapi はJestに toSatisfyApiSpec() と toSatisfySchemaInApiSpec() の2つのマッチャを追加するライブラリです。READMEは対応クライアントとして axios・request-promise・supertest・superagent・chai-http を挙げています。
検証用のExpressアプリと仕様書
以降の例はすべて次の app.js を対象にします。/users/2 だけ、わざと id を文字列で返します。
// app.js
const express = require('express');
const app = express();
app.use(express.json());
app.post('/login', (req, res) => {
if (req.body.user === 'alice' && req.body.password === 'secret') return res.json({ token: 'abc123' });
res.status(401).json({ message: 'invalid credentials' });
});
app.get('/me', (req, res) => {
if (req.get('authorization') !== 'Bearer abc123') return res.status(401).json({ message: 'unauthorized' });
res.json({ id: 1, name: 'Alice' });
});
app.get('/users/:id', (req, res) => {
if (req.params.id === '2') return res.json({ id: '2', name: 'Bob' }); // 仕様違反: id が文字列
if (req.params.id === '3') return res.json({ id: 3, name: 'Carol', nickname: null });
res.json({ id: 1, name: 'Alice' });
});
module.exports = app;
仕様書 openapi30.yaml は /users/{id} だけを定義します。
openapi: 3.0.3
info: { title: Users, version: 1.0.0 }
paths:
/users/{id}:
get:
parameters:
- { name: id, in: path, required: true, schema: { type: integer } }
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/User' }
components:
schemas:
User:
type: object
required: [id, name]
properties:
id: { type: integer }
name: { type: string }
nickname: { type: string, nullable: true }
3.1版の openapi31.yaml は、1行目を openapi: 3.1.0 に、nickname を { type: [string, "null"] } に変えただけのものです。依存は npm install --save-dev jest jest-openapi supertest と npm install express で入ります。
supertestでの最小構成
次のコードを app.js と同じディレクトリに api.test.js として保存します(CommonJS前提)。冒頭で仕様を1回読み込み、レスポンスをマッチャに渡し、npx jest で実行します。
const path = require('path');
const request = require('supertest');
const jestOpenAPI = require('jest-openapi').default;
const app = require('./app');
// 仕様はファイルの絶対パスかオブジェクトで渡す
jestOpenAPI(path.join(__dirname, 'openapi30.yaml'));
test('GET /users/1 は仕様どおり', async () => {
const res = await request(app).get('/users/1');
expect(res.status).toBe(200);
expect(res).toSatisfyApiSpec();
});
test('User スキーマ単体の照合', () => {
expect({ id: 1, name: 'Alice' }).toSatisfySchemaInApiSpec('User');
});
CommonJSで require するときは .default が必要です。付けないと jestOpenAPI is not a function になります。Node.jsのネイティブESMでも同じで、import pkg from 'jest-openapi' の pkg はオブジェクトなので、pkg.default(...) と呼びます。ts-jestやBabelでCommonJSへ変換する構成なら、READMEどおり import jestOpenAPI from 'jest-openapi' で関数を受け取れます。
仕様違反のときに出る失敗メッセージ
/users/2 を照合すると、Jestは次のように落ちます。
expected received to satisfy the '200' response defined for endpoint 'GET /users/{id}' in your API spec
received did not satisfy it because: id must be integer
received contained: { body: { id: '2', name: 'Bob' } }
どのエンドポイントの、どのステータスの定義と、どの項目が合わないかが1回の実行でわかります。仕様に無いパスへのリクエストを照合した場合も your API spec has no matching path で失敗するため、ドキュメントに載っていないエンドポイントがテストに現れれば気づけます。Jest側のモックで依存を差し替える方法はJest mockImplementationの使い方で扱っています。
jest-openapiの保守停止とOpenAPI 3.1非対応
2026年時点で導入前に知っておくべき制約が2つあります。以下は検証環境(Node.js v26.5.0・Jest 30.5.2・jest-openapi 0.14.2)での結果です。
最終リリース0.14.2(2022年1月)と依存のaxios 0.21系
npmレジストリの公開日時は 0.14.2 が2022-01-03で、それ以降の版はありません。開発元の openapi-library/OpenAPIValidators のコミット履歴は2022-03-29のドキュメント修正が最後です(2026-10-12確認)。リポジトリはアーカイブされていませんが、実質的に更新は止まっています。
依存ツリーにも影響があります。jest-openapi が依存する openapi-validator 0.14.2 は axios を ^0.21.1 で指定しているため、インストールされるのは0.21系(検証時は0.21.4)に限られます。このため npm audit はaxiosについてSSRFや認証情報漏えいなどhighの勧告を複数出します。開発時だけ使うパッケージとはいえ、脆弱性ゼロをCIの条件にしている組織では導入時点で引っかかります。
type配列を含む3.1仕様の読み込みエラー
READMEは「OpenAPI 2 and 3」に対応と書いていますが、リンク先は3.0.0の仕様です。内部で使う openapi-schema-validator 9.3.1 は openapi の値を整数に変換して版を判定するため、3.1.0 も「3」とみなし、同梱の3.0用メタスキーマで仕様を検査します。このメタスキーマは type に「文字列であること」しか求めていません。3.1では nullable: true が廃止され、JSON Schema 2020-12に合わせて type: [string, "null"] と配列で書くため、この形を含む仕様を渡すと、テストが1件も走らないうちに止まります。
Invalid OpenAPI spec: [
{
instancePath: '/components/schemas/User/properties/nickname/type',
keyword: 'type',
message: 'must be string'
},
...
]
at validateSpec (node_modules/openapi-validator/lib/openApiSpecFactory.ts:71:11)
一方、type が文字列であれば、3.0に無い値でも通ります。FastAPI 0.143.0は openapi の値として 3.1.0 を出力しますが、Optional[str] を anyOf: [{type: string}, {type: "null"}] で表します。type: "null" は本来3.0の仕様に無い値ですが、メタスキーマは文字列でさえあれば通すため、検証環境では読み込みに成功し、nickname に null を返す応答は通り、数値を返す応答は失敗しました(FastAPI側の仕様の見え方はFastAPIのSwagger UI(/docs)の使い方を参照)。つまり、落ちるかどうかは3.1の書き方を使っているかではなく、同梱メタスキーマの検査に引っかかるかで決まります。読み込めた場合も正式対応ではないため、null許容などの照合結果は、違反する応答を1件用意して本当に落ちるかを確かめてから使ってください。
3.1への公式対応はGitHubのissue #305(2025-12-07起票)で要望されていますが、未対応のまま開いています。OpenAPI自体は仕様書の改訂履歴のとおり3.1.0が2021-02-15、3.2.1が2026-09-10と先へ進んでいます。Vitest向けの vitest-openapi(1.0.3)も依存に jest-openapi ^0.14.2 を持つため、同じ制約を引き継ぎます。
OpenAPI 3.1の応答ボディをJestで照合するAjv 2020の自作マッチャ
3.1のスキーマオブジェクトはJSON Schema 2020-12を拡張したもの(上位集合)です。型・必須項目・$ref の参照といった2020-12の範囲なら、2020-12に対応した Ajv に仕様書を丸ごと登録して応答ボディを検証できます。追加するのは ajv・ajv-formats・js-yaml の3つで、本体の ajv は8.20.0(2026年4月)、js-yaml は5.4.3(2026-10-06)と更新が続いています。
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const request = require('supertest');
const Ajv2020 = require('ajv/dist/2020').default;
const addFormats = require('ajv-formats').default;
const app = require('./app');
const spec = yaml.load(fs.readFileSync(path.join(__dirname, 'openapi31.yaml'), 'utf8'));
// discriminator や example など OpenAPI 独自のキーワードを無視させるため strict: false
const ajv = new Ajv2020({ strict: false, allErrors: true });
addFormats(ajv);
ajv.addSchema(spec, 'spec');
expect.extend({
toMatchResponseSchema(res, specPath, method) {
// JSON Pointer では ~ を ~0、/ を ~1 に置き換える
const ptr = specPath.replace(/~/g, '~0').replace(/\//g, '~1');
const ref = `spec#/paths/${ptr}/${method}/responses/${res.status}/content/application~1json/schema`;
const validate = ajv.getSchema(ref);
if (!validate) {
return { pass: false, message: () => `仕様に ${method.toUpperCase()} ${specPath} の ${res.status} 応答がありません` };
}
const pass = validate(res.body);
return { pass, message: () => ajv.errorsText(validate.errors) };
},
});
test('GET /users/{id} は 3.1 の仕様どおり', async () => {
const res = await request(app).get('/users/3');
expect(res).toMatchResponseSchema('/users/{id}', 'get');
});
同じ3.1の仕様で、nickname: null を返す /users/3 は通り、id を文字列で返す /users/2 は data/id must be integer で失敗しました。スキーマ内の $ref: '#/components/schemas/User' も spec という名前で登録した文書の中で解決されます。
このマッチャは約20行で済む代わりに、汎用の応答バリデーターではありません。次の処理は持たないので、使う仕様がこれらに当たらないかを先に確かめてください。
- リクエストのパスから
/users/{id}のテンプレートを探す処理(テスト側で仕様のパスを明示する) responses直下の$ref、2XXのような範囲指定、default応答へのフォールバック- 実際の
Content-Typeの確認と、application/json以外のメディアタイプ readOnly・writeOnlyの方向の扱い(Ajvは注釈として無視するため、requiredにwriteOnlyのpasswordを含む共有スキーマでは、passwordを返さない正しい応答が必須項目の欠落で落ちる。応答用のスキーマを分ける)- 別ファイルを指す
$refの解決(必要なら先に@apidevtools/swagger-parserで1ファイルに束ねる)
パスの自動照合まで欲しい場合は既製の検証ライブラリが候補になりますが、Jestでの読み込みは事前に試してください。たとえば openapi-backend 5.21.3 は、依存する @apidevtools/json-schema-ref-parser がESM形式のため、Jest 30の既定設定(CommonJS)では Must use import to load ES Module で読み込めませんでした。
Schemathesisによる入力生成と仕様違反の検出
Schemathesis はPython製のCLIで、仕様のパラメータ定義から正常値と異常値を大量に生成し、応答が仕様に合うかを確かめます。公式ドキュメントは対応形式をOpenAPI 2.0(Swagger)・3.0・3.1・3.2とGraphQLとしており、ライセンスはMITです。2026年10月時点の最新は4.30.1(GitHubのリリース日時で2026-10-09 UTC)で、10月の最初の9日間だけで7回リリースされています。
実行コマンドと検出された不備
先に node -e "require('./app').listen(48231, '127.0.0.1')" でアプリを起動しておき、3.1の仕様を渡します。uvを導入しておけば、Pythonの仮想環境を手動で作らずに uvx で実行でき、@ で版を固定すると結果を比べやすくなります。
uvx [email protected] run openapi31.yaml --url http://127.0.0.1:48231 --max-examples 30
検証時は38件のテストケースが生成され(件数は乱数と設定で変わります)、次の不備が1件見つかりました。
- API accepted schema-violating request
Invalid data should have been rejected
Expected: 400, 401, 403, 404, 405, 406, 409, 415, 422, 428, 429, 5xx
Invalid component: parameter `id` in path - incorrect type
Reproduce with:
curl -X GET http://127.0.0.1:48231/users/null%2Cnull
仕様は id を integer と定義しているのに、実装は /users/null,null にも200を返していました。jest-openapiでは見つからない種類の不具合です。手書きでも異常入力の拒否は検証できますが、入力例を自分で列挙する必要があります。一方、Schemathesisは仕様から入力を生成して探索するため、想定していなかった値で「仕様が拒否すべき入力を拒否しているか」を確かめられます。検出項目は --checks で選べ、negative_data_rejection・response_schema_conformance・not_a_server_error などがあります。性質を定義して入力を生成する考え方はプロパティベーステストの書き方と縮小・再現の仕組みで詳しく扱っています。
GitHub Actionsへの組み込み
公式のGitHub Action schemathesis/action@v3(v3.0.0は2026-03-11公開)があり、仕様のURLを渡すだけで動きます。CLIでは --report junit を付けると schemathesis-report ディレクトリにJUnit形式のXMLが出力され、CIのテスト結果画面に載せられます。最初から全項目を必須にすると既存APIの不備がまとめて出てCIが止まるため、--exclude-checks で段階的に有効化するのが現実的です。
StepCIによる複数ステップのシナリオ検証
StepCI はテストをYAMLで宣言するCLIで、ライセンスはMPL-2.0です。前のステップの応答から値を取り出して後続に渡せるため、「ログインしてトークンを得る→そのトークンで自分の情報を取る」という流れを1ファイルで書けます。
トークンを引き回すワークフローYAMLと終了コード
version: "1.1"
name: Login flow
env:
host: http://127.0.0.1:48231
tests:
login:
steps:
- name: login
http:
url: ${{env.host}}/login
method: POST
json:
user: alice
password: secret
captures:
token:
jsonpath: $.token
check:
status: 200
- name: me
http:
url: ${{env.host}}/me
method: GET
auth:
bearer:
token: ${{captures.token}}
check:
status: 200
jsonpath:
$.name: Alice
npx stepci run login.yml で実行すると、captures で取り出したトークンが2つ目のステップの Authorization: Bearer に入り、2ステップとも成功して終了コード0で終わりました。check に schema を書けば応答をJSON Schemaで照合でき、条件を満たさないステップがあると期待値と実際の応答を並べて表示し、終了コード5で終わります。CIのステップに置けばそのまま失敗として扱われます。
stepci generate openapi31.yaml で仕様から雛形も作れ、3.1の type: [string, "null"] もそのまま保持されました。ただし今回の仕様には servers を書いていなかったため、生成されたURLはホスト名を含まない相対パスで、パスパラメータには乱数が入りました。生成後はURLと値を確認してから実行してください。
StepCIの最終リリース2.8.2と利用状況の送信
npmの stepci は2.8.2(2024-06-10)が最新で、GitHubのコミット履歴も2024-08-03のドキュメント修正が最後です(2026-10-12確認)。配布物の dist/lib/analytics.js を読むと、コマンド実行のたびにPostHogへOS名・Node.jsのバージョン・実行したサブコマンド・実行環境(CI名かDockerかローカル)を送信しています。環境変数 STEPCI_DISABLE_ANALYTICS に何か値を入れると止まります。社外への通信に制限がある現場では、CI設定でこの変数を必ず定義してください。
保守が止まっている点を踏まえると、新規に採用するなら、シナリオを既存のJestテストの中でsupertestを続けて呼ぶ形で書くほうが依存を増やしません。GUIで組みたい場合はPostmanとNewmanでのAPIテスト自動化が選択肢になります。
ツールの選び分けと導入を見送る場面
| 道具 | 対応する仕様 | 更新状況(2026-10時点・UTC) | 使う場面 |
|---|---|---|---|
| jest-openapi | 2.0・3.0 | 2022-01-03が最後 | 3.0の仕様と既存Jest |
| Ajv自作マッチャ | 3.1の応答ボディ | Ajvは更新中 | 3.1の仕様と既存Jest |
| Schemathesis | 2.0〜3.2 | 2026-10-09 | 入力の拒否・5xxの検出 |
| StepCI | 3.1から生成可 | 2024-06-10が最後 | YAMLでのシナリオ |
| Dredd | – | アーカイブ済み | 新規採用しない |
以前よく推奨された Dredd はリポジトリがアーカイブされ、読み取り専用になっています。新しい解説記事でも推奨先に挙がっていることがありますが、新規採用の候補から外してください。
次のいずれかに当てはまる現場では、仕様駆動テストの導入を見送るか、先に別の手当てをします。
- 仕様書を更新する担当者がいない。古い仕様を正解にしたテストは正しい実装を落とすため、誤検知が続いて
toSatisfyApiSpecが無効化される原因になります。先にスキーマファーストの運用(仕様の所有者と破壊的変更の検知)を決めます。 - サービス同士の互換性を守りたい。提供側の仕様への適合は、利用側が実際に使う項目の保証にはなりません。利用側起点で確かめるならPactによる消費者駆動契約テストが向きます。
なお、仕様をコードから自動生成している場合でも、実行時の応答が仕様どおりになる保証にはならないため、応答の照合は省かずに続けます。そのうえでSchemathesisによる入力側の検証を足すと、応答照合とは別の不具合も探せます。CIへの組み込みや品質ゲートの設計をまとめて見直したい場合は、DevOps・CI/CD導入支援でテスト自動化を含めて相談を受け付けています。
よくある質問
「openai api vs jest」で調べても比較記事が見つからないのはなぜですか?
OpenAI API(OpenAI社のLLMを呼び出すサービス)とJest(JavaScriptのテストフレームワーク)は用途が別で、比較の対象にならないためです。APIの仕様記述形式である「OpenAPI」を探していた場合は、表記を確かめてください。JestとOpenAPIは競合ではなく、OpenAPIの仕様をJestのテストで照合する組み合わせで使います。
jest-openapiはOpenAPI 3.1の仕様で使えますか?
公式には対応していません。0.14.2は3.1の仕様も3.0の規則で検査するため、type: [string, "null"] のような3.1の書き方を含むと Invalid OpenAPI spec で止まります。FastAPIが出力する anyOf 形式のように読み込めるものもありますが、正式対応ではありません。3.1の仕様は、Ajvの2020-12対応版で応答ボディを照合するか、Schemathesisを併用します。
Swagger 2.0の仕様書でもテストを自動化できますか?
できます。jest-openapiはSwagger 2.0(OpenAPI 2)の仕様を読み込めます。Schemathesisも2.0に対応しています。新しく書く仕様を3.1以降にしておくと、Ajvなど汎用のJSON Schemaツールで扱える範囲が広がります。
StepCIは無料で使えますか?
CLIはMPL-2.0のオープンソースで、npx stepci run で無料で実行できます。ただし2024-06-10の2.8.2を最後にリリースが無く、既定で利用状況を送信するため、CIでは STEPCI_DISABLE_ANALYTICS を設定してください。
JestとPostmanのどちらでAPIテストを書くべきですか?
アプリのリポジトリでコードと一緒にテストを管理し、プルリクエストごとに回すならJestです。コードを書かない担当者がGUIでリクエストを組み、テストとして共有したいならPostmanが向きます。Postmanのテストは newman でCIからも実行できます。