LoginSignup
26
11

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-02

はじめに

グロースエクスパートナーズ 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全てをテストできるようになりました。

参考

サンプルコード

26
11
0

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
  3. You can use dark theme
What you can do with signing up
26
11