APIコントラクトテストの実装方法:信頼性の高いAPIのためのベストプラクティス

Ashley Goolam

Ashley Goolam

18 11月 2025

APIコントラクトテストの実装方法:信頼性の高いAPIのためのベストプラクティス

バックエンドAPIが予期せず変更されたために、フロントエンドアプリケーションが突然機能しなくなった、という状況に遭遇したことはありませんか?そのような中断は、システム全体に波及し、ユーザーの不満や必死のデバッグセッションを引き起こす可能性があります。まさにここでAPI契約テストが重要になります。これは、APIプロデューサーとコンシューマー間の調和を保証する体系的なアプローチです。このガイドでは、API契約テストの基礎、課題、自動化戦略を探りながら、その微妙な点について詳しく説明します。最後まで読めば、API契約テストがいかに単なる「あれば良いもの」ではなく、堅牢なソフトウェア開発の基礎であるかが理解できるでしょう。API契約テストをワークフローにシームレスに組み込む方法を解き明かすために、一緒にこの旅に出かけましょう。

💡
美しいAPIドキュメントを生成する優れたAPIテストツールをお探しですか?

開発チームが最高の生産性で共同作業できる統合されたオールインワンプラットフォームをお探しですか?

Apidogは、お客様のすべての要求に応え、Postmanをはるかに手頃な価格で置き換えます
ボタン

API契約テストとは?

API契約テストは、APIプロバイダーとそのコンシューマー間で合意されたインターフェースが、システム進化の過程で一貫性を保つことを保証します。OpenAPIまたはSwaggerの仕様を通じて定義されるこの契約は、両者が依拠する正式な合意のように、エンドポイント、メソッド、スキーマ、ヘッダー、ステータスコードといった、期待されるリクエストとレスポンスの構造を記述します。

API契約テストは、一般的に2つの形式を取ります。コンシューマー駆動型契約(CDC)は、マイクロサービスにおける統合障害を防ぐため、コンシューマーの視点から期待値を定義します。APIチームによって作成されるプロバイダー定義型契約は、より広範な検証のためにサポートされるすべてのインタラクションをカバーします。機能テストや単体テストとは異なり、これらのテストは基盤となるロジックではなく、APIインターフェースに厳密に焦点を当てます。

API契約テストの重要性

開発ライフサイクルにおいて、API契約テストを優先する理由は何でしょうか?わずかなAPIの変更が、下流で大きな障害を引き起こす可能性があります。早期に契約を検証することで、サービス間の一貫した通信が保証され、問題が本番環境に到達するずっと前に防止されます。

API契約テストの主な利点は以下の通りです。

EコマースやSaaSのような高速で反復的な環境では、安定した、予測可能で、ユーザーフレンドリーなアプリケーションを提供するために、API契約テストが不可欠となります。

手動API契約テストの課題

APIの規模が拡大するにつれて、API契約テストの手動実行はすぐに非現実的になります。リクエストを手動で作成し、仕様と照合してレスポンスをチェックし、ヘッダーやエラーコードを検証する作業は、時間がかかり、エラーが発生しやすく、スケールが困難です。

手動API契約テストの主な課題は以下の通りです。

これらの制約は、高速で信頼性の高いAPI開発において、自動化されたAPI契約テストがいかに不可欠であるかを浮き彫りにします。遅く、一貫性のない手動チェックは、APIの安定性への信頼を損ないます。

ApidogでAPI契約テストを開始する

プロジェクトでAPI契約テストを活用する準備はできていますか?包括的なAPI開発プラットフォームであるApidogは、組み込みのスキーマ検証機能とスクリプト機能を備えており、これを容易にします。理想的な入門点となるでしょう。

ApidogでAPI契約テストを有効にするには、OpenAPI仕様をインポートするか、新しいプロジェクトを作成することから始めます。Apidogはスキーマからテストを自動生成し、セットアップを効率化します。

Apidogで新しいプロジェクトを作成する

実践的な例として、API探索の定番であるApidogのデモ「Pet Store」プロジェクトを使用しましょう。Apidogを起動し、「Demo Pet」プロジェクトを選択し、「/pet/{petId}」GETエンドポイントに移動します(注:クエリは「/get/pets/{id}」を使用していますが、標準のPetstoreに合わせて「/pet/{petId}」です)。左上のドロップダウンメニューから環境を「petstore env」または「localmock」に設定し、

環境を選択する

リクエストを実行します。次のようなレスポンスが返されるはずです。

{
  "id": 1,
  "category": {
    "id": 1,
    "name": "dogs"
  },
  "name": "doggie",
  "photoUrls": [],
  "tags": [],
  "status": "available"
}

これにより、契約検証の準備が整います。

「テストケース」タブに移動し、「ケースを追加」をクリックして新しいスイートを作成します。

テストケースを追加

プリプロセッサセクションで、JSONスキーマと変数を定義するカスタムスクリプトを追加します。

ステップ1:プリプロセッサに移動する

Apidogでカスタムテストスクリプトを作成する

ステップ2:カスタムJSコードを追加する

ApidogでカスタムJSテストスクリプトコードを追加する
// petIdが設定されていない場合(文字列として)設定
if (!pm.environment.get("petId")) {
  pm.environment.set("petId", "1");
}

// ペットレスポンス用のJSONスキーマを定義
const petSchema = {
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "id": { "type": "integer" },
    "category": {
      "type": "object",
      "properties": {
        "id": { "type": "integer" },
        "name": { "type": "string" }
      },
      "required": ["id", "name"],
      "additionalProperties": true
    },
    "name": { "type": "string" },
    "photoUrls": {
      "type": "array",
      "items": { "type": "string", "format": "uri" },
      "minItems": 0
    },
    "tags": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "name": { "type": "string" }
        },
        "required": ["id", "name"],
        "additionalProperties": true
      },
      "minItems": 0
    },
    "status": {
      "type": "string",
      "enum": ["available", "pending", "sold"]
    }
  },
  "required": ["id", "name", "photoUrls", "status"],
  "additionalProperties": true
};

// スキーマを環境変数に保存(文字列化)
pm.environment.set("pet_schema", JSON.stringify(petSchema));

// (オプション) デバッグ用にコンソールにログ出力
console.log("Pre-processor: petId =", pm.environment.get("petId"));

次に、ポストプロセッサに検証スクリプトを貼り付けます。

カスタムポストプロセッサースクリプトを追加する
// スキーマ検証にAJVを使用
var Ajv = require('ajv');
var ajv = new Ajv({ allErrors: true, logger: console });

// 環境からスキーマを取得
var raw = pm.environment.get("pet_schema");
var schema;
try {
  schema = (typeof raw === 'string') ? JSON.parse(raw) : raw;
} catch (err) {
  pm.test('Schema is valid JSON', function() {
    pm.expect(false, 'pet_schema is not valid JSON: ' + err.message).to.be.true;
  });
  // 以降のテストを停止
  return;
}

// レスポンスボディをJSONとしてパース
var responseData;
try {
  responseData = pm.response.json();
} catch (err) {
  pm.test('Response is valid JSON', function() {
    pm.expect(false, 'Response body is not JSON: ' + err.message).to.be.true;
  });
  return;
}

// ステータスコードをテスト
pm.test('Status code is 200', function() {
  pm.expect(pm.response.status).to.eql("OK");
});

// スキーマを検証
pm.test('Response matches pet schema', function() {
  var valid = ajv.validate(schema, responseData);
  if (!valid) {
    console.log('AJV Errors:', ajv.errors);
  }
  pm.expect(valid, 'Response does not match schema, see console for errors').to.be.true;
});

// 追加のアサーション
pm.test('Returned id matches requested petId', function() {
  var requested = pm.environment.get("petId");
  // petIdは文字列として保存されているが、レスポンスのidは整数
  var requestedNum = Number(requested);
  if (!isNaN(requestedNum)) {
    pm.expect(responseData.id).to.eql(requestedNum);
  } else {
    pm.expect(String(responseData.id)).to.eql(String(requested));
  }
});

pm.test('Name is a string', function() {
  pm.expect(responseData.name).to.be.a('string');
});

pm.test('Status is one of expected values', function() {
  pm.expect(responseData.status).to.be.oneOf(['available', 'pending', 'sold']);
});

// オプション: より詳細なチェック(category、photoUrls、tags)
pm.test('Category has id and name', function() {
  pm.expect(responseData.category).to.have.property('id');
  pm.expect(responseData.category).to.have.property('name');
});

pm.test('At least one photo URL', function() {
  pm.expect(responseData.photoUrls).to.be.an('array').that.is.not.empty;
});

pm.test('Tags are valid objects', function() {
  pm.expect(responseData.tags).to.be.an('array');
  if (responseData.tags.length > 0) {
    responseData.tags.forEach(function(tag) {
      pm.expect(tag).to.have.property('id');
      pm.expect(tag).to.have.property('name');
    });
  }
});

「実行」をクリックして実行します。Apidogは右側に「合格」または「失敗」の結果と、展開可能な詳細を表示します。成功した場合の実行結果は次のようになるでしょう。

APIレスポンス:

APIレスポンス
{
  "id": 1,
  "category": {
    "id": 1,
    "name": "dog"
  },
  "name": "Jasper",
  "photoUrls": [
    "https://loremflickr.com/400/400?lock=7187959506185006"
  ],
  "tags": [
    {
      "id": 3,
      "name": "Yellow"
    }
  ],
  "status": "available"
}

合格したテスト:

  1. ステータスコードは200
  2. レスポンスはペットスキーマに一致
  3. 返されたidはリクエストされたpetIdに一致
  4. Nameは文字列
  5. Statusは予期される値のいずれか
  6. Categoryはidとnameを持つ
  7. 少なくとも1つの写真URL
  8. タグは有効なオブジェクト
API契約テスト結果を表示する

失敗をシミュレートするには、ステータスコードのテストを「OK」ではなく数値(200)を期待するように変更し、

JSテストスクリプトを変更する

再実行してアサーションエラーを確認します。

アサーションエラー

回帰実行のためにスイートを保存します。Apidogの直感的なインターフェースは、スキーマチェックのためのAJV統合により、API契約テストを民主化し、複雑な検証を日常的なタスクに変えます。

よくある質問

Q1. API契約テストと統合テストの違いは何ですか?

回答: API契約テストは、ビジネスロジックを実行せずにインターフェース契約を検証しますが、統合テストは、データフローや依存関係を含め、サービスがどのように相互作用するかを検査します。

Q2. API契約テストはGraphQL APIにも適用できますか?

回答: はい、主にREST用に設計されていますが、PactのようなツールはGraphQLスキーマをサポートしており、クエリ/レスポンス構造とミューテーションに焦点を当てています。

Q3. CI/CDパイプラインでAPI契約テストはどのくらいの頻度で実行すべきですか?

回答: 理想的には、問題を早期に発見するためにコミットまたはプルリクエストごとに実行し、包括的なカバレッジのために夜間実行も行います。

Q4. 契約テスト用のOpenAPI仕様がチームにない場合はどうすればよいですか?

回答: まず、Swagger Codegenのようなツールを使用して既存のコードから生成し、その後共同で改善してベースラインを確立します。

Q5. API契約テストはレガシーAPIにも適していますか?

回答: もちろんです。現在の動作を文書化するために仕様をレトロフィットし、その後テストを自動化してモダナイゼーション中のリグレッションから保護します。

結論

今回の探求を終えるにあたり、API契約テストが、相互接続された世界において信頼性が高く、スケーラブルなAPIの要であることが明らかになります。手動作業の苦痛を軽減し、自動化された保護策を強化することで、チームは恐れることなくイノベーションを進めることができます。Apidogのようなツールを活用して実践を向上させ、アプリケーションが回復力と効率性を獲得するのを見てください。既存の契約を洗練させるにせよ、新しい契約を構築するにせよ、優れたAPIガバナンスへの道は、単一の明確に定義されたテストから始まります。

💡
美しいAPIドキュメントを生成する優れたAPIテストツールをお探しですか?

開発チームが最高の生産性で共同作業できる統合されたオールインワンプラットフォームをお探しですか?

Apidogは、お客様のすべての要求に応え、Postmanをはるかに手頃な価格で置き換えます
ボタン

ApidogでAPIデザイン中心のアプローチを取る

APIの開発と利用をよりシンプルなことにする方法を発見できる