LoginSignup
7

More than 1 year has passed since last update.

Organization

OpenAPIドキュメントから全APIの自動テストを生成する

はじめに

グロースエクスパートナーズ Advent Calendar 2020 3日目。
yitoです。

API開発をしている中で、テストするのに毎回クライアントツールにリクエストを用意してAPIを呼び出すのが手間でした。
それを、コマンド一発で、API仕様書に定義した全てのAPIをテストできるようにしたので、その方法を紹介します。

概要

OpenAPIドキュメントをAPIクライアントツールで利用可能なJSONに変換し、それをCLIライブラリで実行します。
これを実現する、以下について順番に説明していきます。

  • Postmanについて
  • OpenAPI 3.0 to Postman Collection v2.1.0 Converter
  • Postman Collection SDK
  • newman

Postmanについて

APIリクエストのクライアントツールにPostmanがあります。
Postmanは、以下のようなことができます。

スクリーンショット 2020-11-26 0.00.35.png

書き出したリクエスト情報を保存すると、画面左側にあるPostman Collectionsに追加され、フォルダでの管理ができます。
このPostman Collectionsは、Webに公開する(制限をかけることも可能?)か、JSONでのexport/importで、開発チーム内で共有が可能です。

OpenAPI 3.0 to Postman Collection v2.1.0 Converter

OpenAPI 3.0 to Postman Collection v2.1.0 Converterは、OpenAPIドキュメントをPostman CollectionのJSONに変換する、NodeJSで実行可能なライブラリです。
使い方は簡単で、OpenAPIのyamlを読み込んで、このライブラリのクラスに渡して実行するだけです。


const fs = require('fs');
const readFileData = fs.readFileSync('OpenAPIドキュメントのファイルパス', {encoding: 'UTF8'});

const Converter = require('openapi-to-postmanv2');
Converter.convert({type: 'string', data: readFileData}, {},
    (err, conversionResult) => {
      if (!conversionResult.result) {
        console.error('API定義をPostman Collectionに変換できませんでした',
            conversionResult.reason);
        return;
      }

      const convertData = conversionResult.output[0].data;

      // Postman CollectionsのJSONファイルを生成する
      fs.writeFile('postman-collections.json', JSON.stringify(convertData, null, 4), (err) => {
        if(err) console.error(err)
      });

    }
);

この生成されたJSONファイルを読み込むと、Postmanにて利用ができます。

スクリーンショット 2020-11-26 0.43.57.png

ただ、このままだと、各APIのリクエストパラメータが <long> <string> のようになっていて、このままだと呼び出したいAPIのパラメータとしては不適切なので、使う際は書き換えが必要です。

手で1つ1つ直すのは面倒なので、Postman Collection SDKを使います

Postman Collection SDK

Postman Collection SDKは、Postman Collectionの生成・更新のための、NodeJSで実行可能なSDKです。
OpenAPI 3.0 to Postman Collection v2.1.0 Converterにて生成したPostman CollectionのJSONを、このSDKを使って編集します。

Postman CollectionのJSONファイルを読み込み、SDKにてCollectionをnewすると、以下のようなクラスが生成されます。
クラスについての詳細

  • Collection
    • Postman Collectionの1つに相当
    • メンバーにItemGroupを持つ
  • ItemGroup
    • Postman Collectionの1フォルダに相当
    • メンバーにItemを持つ
  • Item
    • Postman Collectionの1つのAPIに相当
    • メンバーにRequestを持つ
  • Request
    • APIのURL、リクエストパラメータに関する値
    • メンバーにHeaderList, EventList, VariableListなどを持つ

<long> <string> のような値を実際に使いたい値に変えるため、 Request の値を書き換えます。
こちらにあるOpenAPIドキュメント(api-spec.yml)、Postman Collection SDKを使った実装(postman-test/index.js)を使いながら説明します。

先ほどの実装に以下を追加します。


...
      const convertData = conversionResult.output[0].data;

      const collection = new Collection(convertData);

      collection.items.all().forEach(item => updateItem(item))

      // Postman CollectionsのJSONファイルを生成する
      fs.writeFile('postman-collections.json', JSON.stringify(collection, null, 4), (err) => {
        if(err) console.error(err)
      });

...

collection.itemsItemGroup または Item があります。そこから、 Request を取り出して、 <long> <string> のような値を、使いたい値に書き換えます。


...

const { v4: uuidv4 } = require('uuid');

const updateRequestVariable = (request, key, value) => {
  const findVariable = request.url.variables.find(
      variable => variable.key === key);
  if (findVariable) {
    findVariable.update({
      key: key,
      value: value
    })
  }
}

const updateRequestQuery = (request, key, value) => {
  const findQueryParam = request.url.query.find(
      queryParam => queryParam.key === key);
  if (findQueryParam) {
    findQueryParam.update({
      key: key,
      value: value
    })
  }
}

const updateRequest = (request) => {

  // Authorization
  if (request.auth && request.auth.type === 'apikey') {
    request.auth.update({
      "key": "api_key",
      "value": `api-key-${uuidv4()}`
    }, 'apikey')
  }

  // 書籍関連のAPI
  if (request.url.getPath().includes('books')) {
    updateRequestVariable(request, 'bookId', '100000001');
  }

  // ユーザー関連のAPI
  if (request.url.getPath().includes('users')) {
    updateRequestVariable(request, 'username', 'user-XXXX');
    updateRequestQuery(request, 'username', 'user-XXXX');
    updateRequestQuery(request, 'password', 'pass-XXXX');

  }

  return request;
}

const updateItem = (item) => {
  if (item.items) {
    item.items.all().forEach(item => updateItem(item));
  } else {
    item.request = updateRequest(item.request);

  }

  return item;
}

...

レスポンスに対するテストも実行したいので、 updateItem メソッドに以下のコードを追加します。


...

const updateItem = (item) => {
  if (item.items) {
    item.items.all().forEach(item => updateItem(item));
  } else {
    // 追加
    item.events.add({
      listen: 'test',
      script: {
        exec: "pm.test('response 200 test', () => {\n"
            + "    pm.response.to.have.status(200);\n"
            + "});"
      },
      type: 'text/javascript'
    })

...

これでリクエストパラメータの書き換えがされた状態で、Postman CollectionのJSONが生成されます。

毎回JSONをimportしてAPIを呼び出すのは面倒なので、CLIだけで実行できるように、newmanを使います.

newman

newmanは、CLIでPostman CollectionのJSONを読み込みAPIをリクエストする、NodeJSで実行可能なライブラリです。

先ほどのコードで生成したJSONを、newmanで実行するように変えます。

...
      const collection = new Collection(convertData);

      collection.items.all().forEach(item => updateItem(item))
      fs.writeFile('postman-collections.json', JSON.stringify(collection, null, 4), (err) => {
        if(err) console.error(err)
      });

      newman.run({
        collection: collection,
        reporters: 'cli',
        environment: require('./local.postman_environment.json')
      }, (err) => {
        if (err) console.error(err);
      });

...

  • newman.run()
    • collection: Postman CollectionのJSONを指定
    • reports: 実行結果の出力方法の指定。CLI、Junit、JSON等が可能
    • envioroment: Postmanには環境ごとの変数の指定があり、生成されたJSONには baseUrl の指定が必要なため、向き先を指定したJSONを用意して読み込む

実行すると以下のようになります。

% npm run start
> node index.js

newman

Book Management

❏ books
↳ 書籍更新API
  PUT http://localhost:8080/v1/books [200 OK, 123B, 235ms]
  ✓  response 200 test

↳ 書籍登録API
  POST http://localhost:8080/v1/books [200 OK, 123B, 19ms]
  ✓  response 200 test

↳ 書籍一覧取得API
  GET http://localhost:8080/v1/books [200 OK, 800B, 11ms]
  ✓  response 200 test

↳ タグ絞り込み検索API
  GET http://localhost:8080/v1/books/findByTags?tags=<string>&tags=<string> [200 OK, 800B, 14ms]
  ✓  response 200 test

❏ books / {book Id}
↳ 書籍詳細取得API
  GET http://localhost:8080/v1/books/100000001 [200 OK, 290B, 10ms]
  ✓  response 200 test

↳ 書籍削除API
  DELETE http://localhost:8080/v1/books/100000001 [200 OK, 123B, 7ms]
  ✓  response 200 test

❏ users
↳ ユーザー登録API
  POST http://localhost:8080/v1/users [200 OK, 123B, 8ms]
  ✓  response 200 test

↳ ユーザー一覧取得API
  GET http://localhost:8080/v1/users [200 OK, 123B, 6ms]
  ✓  response 200 test

↳ ログインAPI
  GET http://localhost:8080/v1/users/login?username=user-XXXX&password=pass-XXXX [200 OK, 123B, 8ms]
  ✓  response 200 test

↳ ログアウトAPI
  GET http://localhost:8080/v1/users/logout [200 OK, 123B, 6ms]
  ✓  response 200 test

❏ users / {username}
↳ ユーザー詳細取得APi
  GET http://localhost:8080/v1/users/user-XXXX [200 OK, 123B, 6ms]
  ✓  response 200 test

↳ ユーザー更新API
  PUT http://localhost:8080/v1/users/user-XXXX [200 OK, 123B, 5ms]
  ✓  response 200 test

↳ ユーザー削除API
  DELETE http://localhost:8080/v1/users/user-XXXX [200 OK, 123B, 7ms]
  ✓  response 200 test

→ タグ一覧取得API
  GET http://localhost:8080/v1/tags [200 OK, 123B, 6ms]
  ✓  response 200 test

┌─────────────────────────┬───────────────────┬──────────────────┐
│                         │          executed │           failed │
├─────────────────────────┼───────────────────┼──────────────────┤
│              iterations │                 1 │                0 │
├─────────────────────────┼───────────────────┼──────────────────┤
│                requests │                14 │                0 │
├─────────────────────────┼───────────────────┼──────────────────┤
│            test-scripts │                14 │                0 │
├─────────────────────────┼───────────────────┼──────────────────┤
│      prerequest-scripts │                 0 │                0 │
├─────────────────────────┼───────────────────┼──────────────────┤
│              assertions │                14 │                0 │
├─────────────────────────┴───────────────────┴──────────────────┤
│ total run duration: 654ms                                      │
├────────────────────────────────────────────────────────────────┤
│ total data received: 1.37KB (approx)                           │
├────────────────────────────────────────────────────────────────┤
│ average response time: 24ms [min: 5ms, max: 235ms, s.d.: 58ms] │
└────────────────────────────────────────────────────────────────┘

以上で、コマンド一発でAPI全てをテストできるようになりました。

参考

サンプルコード

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
7