API サーバーを構築する際に swagger (https://swagger.io/) を用いて仕様を定義することが増えてきているように見えますが、せっかく DSL ベースで API 仕様を表現しているのですから、その情報を用いて色々な作業を楽にしたいですよね。

この記事では Swagger とその周辺ツールを用いて出来ることの一つとして、API のテストツールとしてデファクトスタンダードに近づいてきた postman (https://www.getpostman.com/) と連携して自動 API テストを試みます。

サンプルファイル

はじめに、元になるサンプルファイルがあった方が分かりやすいと思うので、以下例示します。なおファイル中に書かれている事に特に意味はないので、細かいツッコミは無用です。

swagger.json
{
  "swagger" : "2.0",
  "info" : {
    "description" : "Sada API",
    "version" : "1.0.0",
    "title" : "Sada"
  },
  "host" : "localhost:8080",
  "basePath" : "/api/v1",
  "schemes" : [ "http" ],
  "produces" : [ "application/json" ],
  "paths" : {
    "/sada/{sada_id}" : {
      "get" : {
        "tags" : [ "sada" ],
        "summary" : "sada\n",
        "schemes" : [ "http" ],
        "parameters" : [ {
          "name" : "sada_id",
          "in" : "path",
          "description" : "sada ID",
          "required" : true,
          "type" : "integer",
          "minimum" : 1,
          "format" : "int64"
        } ],
        "responses" : {
          "200" : {
            "description" : "200 (OK)",
            "schema" : {
              "$ref" : "#/definitions/sadaResponse"
            }
          }
        }
      }
    }
  },
  "definitions" : {
    "sada" : {
      "type" : "object",
      "properties" : {
        "id" : {
          "type" : "integer",
          "format" : "int64",
          "example" : 1
        },
        "name" : {
          "type" : "string",
          "example" : "masashi"
        },
        "age" : {
          "type" : "integer",
          "format" : "int32",
          "example" : 65
        }
      }
    },
    "sadaResponse" : {
      "properties" : {
        "sada" : {
          "type" : "array",
          "items" : {
            "$ref" : "#/definitions/sada"
          }
        }
      }
    }
  },
  "parameters" : {
    "sada_id" : {
      "name" : "sada_id",
      "in" : "path",
      "description" : "sada ID",
      "required" : true,
      "type" : "integer",
      "minimum" : 1,
      "format" : "int64"
    }
  }
}

/sada というエンドポイントに id 付きでリクエストすると、 masashi という文字列を含むレスポンスが返却される、ごく一般的な API です。

swagger2-to-postman を用いて swagger から postman の設定ファイルを生成

Swagger の API 仕様を記述した JSON / YAML ファイルから、postman のテスト定義ファイルである JSON ファイルを生成するツールとして、postmanlabs が提供しているツールで swagger2-to-postman というものがあります。ここではこちらを使います。
https://github.com/postmanlabs/swagger2-to-postman

なお、現状「swagger2」という名前でも分かる通り 2.0 の仕様までしか対応していないので、上記のサンプル含めこの記事では Swagger 2.0 をベースとして話を進めていきます。

コマンドラインの実装

swagger2-to-postman は現在のところコマンドラインツールは提供されていないように見えるので、コマンドラインで実行する際はちょっとした以下のような薄いラッパーを実装してあげる必要があります。nodejs で実装されているので、ラッパーも nodejs で記述します。

cli.js

#!/usr/bin/env node
program = require('commander')
Swagger2ToPostman = require('swagger2-to-postman')

convert = (input) => {
  var fs = require('fs');
  fs.readFile(input, 'utf8', function (err, text) {
    var result = new Swagger2ToPostman().converter(JSON.parse(text));
    console.log(JSON.stringify(result.collection));
  });
}

program
  .option('-i, --input <input_file>', 'swagger.json')
  .parse(process.argv)

convert(program.input)

こちらを用いて実行すると、postman の JSON ファイルが生成されます。


$ ./cli.js -i swagger.json > postman.json

$ cat postman.json | jq "."
{
  "id": "08bf281d-b933-4f4b-a80a-124793abd103",
  "name": "Sada",
  "description": "Sada API",
  "order": [],
  "folders": [
    {
      "id": "21e27ddf-8df8-4656-9519-64708b461782",
      "name": "sada",
      "description": "Folder for sada",
      "order": [
        "aa2ba412-8e87-4c95-b4ac-ef2e900a953d"
      ],
      "collection_name": "Sada",
      "collection_id": "08bf281d-b933-4f4b-a80a-124793abd103",
      "collection": "08bf281d-b933-4f4b-a80a-124793abd103"
    }
  ],
  "timestamp": 1413302258635,
  "synced": false,
  "requests": [
    {
      "id": "aa2ba412-8e87-4c95-b4ac-ef2e900a953d",
      "headers": "Accept: application/json\n",
      "url": "http://localhost:8080/api/v1/sada/:sada_id",
      "pathVariables": {
        "sada_id": "{{sada_id}}"
      },
      "preRequestScript": "",
      "method": "GET",
      "data": [],
      "rawModeData": null,
      "dataMode": "params",
      "description": "",
      "descriptionFormat": "html",
      "time": 1512116271923,
      "version": 2,
      "responses": [],
      "tests": "",
      "collectionId": "08bf281d-b933-4f4b-a80a-124793abd103",
      "synced": false,
      "name": "sada\n"
    }
  ]
}

自動生成されたファイルの注意

自動生成されたファイルは以下のような仕様になっているので、若干注意が必要になります。

tests の内容

tests (テスト時の assertion の内容) は常に空文字が設定されます。 この状態で postman を実行すると API がどのような振る舞いをしても test OK になるので、必要に応じて内容を記述します。
一律、HTTP ステータス 200 が帰ってくれば OK なのであれば、以下のような記述を tests に追加するようコマンドラインの中で処理してあげても良いかもしれません。

tests['response code is 200'] = (responseCode.code === 200);

API リクエストパラメータの扱い

Swagger で定義したリクエストパラメータは、生成された postman の設定ファイルでは以下のようなルールで扱われます。

  • Swagger で default 値が設定されている場合 : postman の設定ファイルに default 値が挿入される
  • Swagger で default 値が設定されていない場合 : {{name}} という形でブラケットに括られる形で挿入される

上記サンプルでいうと、以下の部分。

"url": "http://localhost:8080/api/v1/sada/:sada_id",
"pathVariables": {
  "sada_id": "{{sada_id}}"
},

ブラケットの値は、実行時に値を設定することができます。こちらは後述します。

newman による postman のコマンドライン実行

上記で生成した postman の設定ファイルを元に API テストを自動実行させるのが目的ですので、何かしら postman をコマンドラインで実行する環境が必要です。

幸い postmanlabs は newman というコマンドラインツールを提供しているので、こちらを使用します。
https://github.com/postmanlabs/newman

インストール

newman も nodejs で実装されているので、nodejs の実行環境が必要です。
公式ドキュメントに則ると、以下のような形でインストールします。

$ npm install newman --global;

実行

newman run <config file> <options> とすることでコマンドラインで postman を実行することができます。


$ newman run postman.json
newman

Sada

❏ sada
↳ sada

  GET http://localhost:8080/api/v1/sada/{{sada_id}} [400 Bad Request, 2.4KB, 19ms]
  1. response code is 200

┌─────────────────────────┬──────────┬──────────┐
│                         │ executed │   failed │
├─────────────────────────┼──────────┼──────────┤
│              iterations │        1 │        0 │
├─────────────────────────┼──────────┼──────────┤
│                requests │        1 │        0 │
├─────────────────────────┼──────────┼──────────┤
│            test-scripts │        1 │        0 │
├─────────────────────────┼──────────┼──────────┤
│      prerequest-scripts │        0 │        0 │
├─────────────────────────┼──────────┼──────────┤
│              assertions │        1 │        1 │
├─────────────────────────┴──────────┴──────────┤
│ total run duration: 87ms                      │
├───────────────────────────────────────────────┤
│ total data received: 2.18KB (approx)          │
├───────────────────────────────────────────────┤
│ average response time: 19ms                   │
└───────────────────────────────────────────────┘

  #  failure                   detail                                                                                               

 1.  AssertionFailure          response code is 200                                                                                 
                               at assertion:1 in test-script                                                                        
                               inside "sada / sada

上記の例では API が HTTP ステータス 400 を返却し、期待する結果(200)と異なるため assertions が failed になっています。上述のとおり、id を渡す箇所がブラケット記述になっているためで、こちらは実行時に適切な値を注入する必要があります。

postman の JSON ファイルに直接書いても良いのですが、 以下のように注入する値を管理する JSON ファイルを用意し、コマンドライン実行時に指定することで、指定した JSON ファイルに含まれる値を使用してくれるようになります。

globals.json
{
  "name": "globals",
  "values": [{
    "key": "sada_id",
    "value": "1"
  }]
}

$ newman run postman.json -g globals.json
newman

Sada

❏ sada
↳ sada

  GET http://localhost:8080/api/v1/sada/1 [200 OK, 219B, 27ms]
  ✓  response code is 200

┌─────────────────────────┬──────────┬──────────┐
│                         │ executed │   failed │
├─────────────────────────┼──────────┼──────────┤
│              iterations │        1 │        0 │
├─────────────────────────┼──────────┼──────────┤
│                requests │        1 │        0 │
├─────────────────────────┼──────────┼──────────┤
│            test-scripts │        1 │        0 │
├─────────────────────────┼──────────┼──────────┤
│      prerequest-scripts │        0 │        0 │
├─────────────────────────┼──────────┼──────────┤
│              assertions │        1 │        0 │
├─────────────────────────┴──────────┴──────────┤
│ total run duration: 98ms                      │
├───────────────────────────────────────────────┤
│ total data received: 87B (approx)             │
├───────────────────────────────────────────────┤
│ average response time: 27ms                   │
└───────────────────────────────────────────────┘

ID の値が実行時に適切に注入されたことで、postman のテストが無事通過しました。

レポート

コマンドラインで実行することで、テストの自動実行のみならず、レポートの管理も自動的に行うことができるようになります。

newman のオプションでは現在、以下のようなレポート形式の出力がサポートされているようです。

  • cli (default)
  • json
  • html
  • junit

一例として、HTML レポートのイメージを添付します。


$ newman run postman.json -g globals.json --reporters cli,html
newman
(snip.)

$ open newman/*.html

image.png

API テスト自動実行に向けて

これで Swagger の設定情報を基に postman で API テストを自動実行するためのツール群が揃いました。上記の内容を踏まえ、API テストの自動実行を試みます。

以下は、イメージ図です。

image.png

フロー

1.開発者が swagger の設定ファイルを git リポジトリに push

文章のとおりです。これをトリガーとして後段の処理が自動で行われていきます。

2. CI ツールが docker イメージを作成し docker リポジトリに push

git リポジトリへの push をトリガーに、以下の処理を CI ツール上で行います。

  • API スタブの自動生成。生成したスタブは API 仕様のドキュメントサーバーとしても用いる。
    • アプリケーション・サーバーとして動作させる。
  • postman の設定ファイルの自動生成。実行環境の newman とともに docker イメージとして固める。
    • バッチ的に定期スケジュール実行をさせる。

CI ツールはこれらを docker build した上で docker リポジトリに push し、必要に応じてサーバー上にデプロイします。

3. スケジューラーが postman のテストを定期実行

postman の設定ファイル入りの docker イメージを起動し、実装済みの API サーバー に対して postman のテストを実行します。
実行頻度は開発プロセスによると思いますが、1日に1回程度をイメージしています。実際に実行するテストは、自動生成したものに加え、API の実装内容に応じてカスタマイズした定義ファイルもあわせて実行することになります。
実行結果は、必要に応じて外部システムへ通知します。

4. 外部システムへの実行結果の通知

図中では実行結果を Slack に通知し、オブジェクトストレージなどにレポートの HTML ファイルを配置する、ということをイメージして書いています。

他に、定期実行の頻度を短くして、Assertion の結果をメトリクス情報として Datadog のような Monitoring ツールに通知する、みたいなことも考えても良いかもしれません。

postman のメンテナンス

実際のところ、swagger から自動生成した postman の設定ファイルを基にすべての API のテストを網羅することは難しいと思います。理由は以下。

  • データの追加・更新・削除を伴う処理を実施したい場合、swagger から生成すると API のテスト順序を定義することができないため、意図通りのテストが行いづらい。
  • 画一的なルールのテスト実施以外は行いづらい。

そのため、割り切りとして以下のような形で運用するのが良いと思っています。

  • swagger から生成した postman の設定ファイルでは、参照系 の API の確認だけ行う。ここでは、ステータスコードが 200 を返すかどうかのみを検証対象とする。
  • 参照系のもののうち、複雑な入力・出力チェックを行う場合は、手で設定ファイルを作成する。
  • 追加・更新・削除系の API の検証は手で設定ファイルを作成し、自動生成したものとあわせてテストを実施する。

その他の選択肢

Swagger を活用した API 自動テストでは、他にも Consumer-Driven Based Testing の考え方に基づいたテストの仕方がありえるかなと思います。
クライアント側・サーバー側どちらがドライバーになるかは現場により様々なので厳密な意味で CDBT になるかはわかりませんが、ツールとしてクライアント側・サーバー側の実装内容のズレを把握する手段としては有効と思います。

Swagger に対応したツールとしては、いくつかオープンソース群が見当たります。

CDBT は運用負荷が高くなりがちでなかなか継続が難しいという話をよく聞きますが、自動化プロセスに載せることでその痛みを多少でも和らげることが可能かもしれません。

しかし現実的な視点だと、開発プロセスの最初から組み込まないと CDBT は適用が大変なイメージがやはりあります。既存のシステムにおいては、なんとか頑張って swagger で仕様を定義し直し、postman などを用いて外部からの動作確認を行う、というやり方の方が穏当かなという感じもします。

まとめにかえて

マイクロサービスという言葉が喧伝されていることもあり、 API サーバーをインターフェースとしてシステムを疎結合化する流れを多くの開発現場で感じます。

そういったシステムではサービスの状況を正しく Monitoring し Tracing することがとても大事で、監視ツールや Distirbuted Tracing のツール、ロギング、Service Discovery の仕組みもろもろももちろん大事ですが、システムに対して正しく情報を把握することは実際に動作しているサーバー群に対してだけでなく、開発プロセスの中でも必要なことになると思います。

API サーバーは力わざで実装してしまうとどうしても動作確認が面倒でつい疎かになりがちですが、ここに書いたようなテストの実行やレポーティングを自動化することにより、複雑化が進む開発の現場で少しでも開発効率が向上し開発者の心の安寧が訪れるとよいな、と思います。