Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9134

👷テストツール dredd👷 openapi の内容でテストを実行する

$
0
0
前職で使っていたツールの dredd について書いてみたいと思います。 dredd 公式サイト: Dredd — HTTP API Testing Framework — Dredd latest documentation 公式サイトより、issue のほうがいろいろ情報あるかも: Issues · apiaryio/dredd dredd は openapi の仕様書をもとに、テストを自動で行ってるツールです。 (※api blueprint で書かれた仕様書でもOK) すでに、openapi の仕様書を管理しているプロジェクトであれば、すぐに導入してテスト自動化ができます。 ※openapi は v2 も v3 も対応していますが、v3 はまだ試験的な導入らしいので、場合によっては正しく動作しない可能性あり。詳しくは公式のドキュメント参照。 Dredd — HTTP API Testing Framework — Dredd latest documentation dredd を使うメリットは、ざっくりこんな感じかと思います 👇 dredd を使うメリット 👍 テストコードを書かなくていい(PHPUnit とか) CLI なので、CI に組み込みやすい openapi などの仕様書をメンテナンスする動機ができる ドキュメントのメンテは放置されがち・・・ テストが簡単に行えるなら、ドキュメントちゃんと書きたくなる 他の開発メンバーも、ドキュメントからサーバーの挙動が把握できるようになる dredd を使ってみる 事前準備 dredd を使うため、下記のエンドポイントを提供するサーバーを用意します。 # ユーザーのリストを取得 GET /users # ユーザーを作成 POST /users # 特定のユーザーを取得 GET /users/{userId} # 特定のユーザーの削除 DELETE /users/{userId} # 特定のユーザーの更新 PATCH /users/{userId} 上記のエンドポイントを定義した openapi の sample-app-api.yml は下記です。 sample-app-api.yml openapi: 3.0.3 info: version: 1.0.0 title: ただのサンプルAPI servers: - url: http://localhost:{port} variables: port: default: "5000" enum: - "5000" components: schemas: userObject: type: object properties: firstName: type: string lastName: type: string age: type: integer id: type: string responseObject: type: object properties: message: type: string body: type: object properties: firstName: type: string lastName: type: string age: type: integer id: type: string responseObjectOnError: type: object properties: message: type: string body: type: object updateCreateUserRequestObject: type: object properties: firstName: type: string lastName: type: string age: type: integer parameters: userIdInPath: in: path name: userId schema: type: string required: true example: 9999999999-999999999-99999999 securitySchemes: basicAuth: type: http scheme: basic security: - basicAuth: [] paths: /users: get: responses: '200': description: ユーザーのリストを返す content: application/json; charset=utf-8: schema: "$ref": "#/components/schemas/userObject" example: - firstName: Jorge lastName: Washignton age: 89 id: 9999999999-999999999-99999999 post: requestBody: content: application/json: schema: "$ref": "#/components/schemas/updateCreateUserRequestObject" example: firstName: hoge lastName: fuga age: 33 responses: '201': description: ユーザーが作成された content: application/json; charset=utf-8: schema: "$ref": "#/components/schemas/responseObject" example: message: "ユーザーID: 19f64be0-fd1a-4e47-a569-65970abf2827 のユーザーを作成しました" body: firstName: hoge lastName: fuga age: 33 id: f9675c3b-fef0-4db6-9728-17e8fb0a6145 '415': description: リクエストで送信するフォーマットが不正 content: application/json; charset=utf-8: schema: "$ref": "#/components/schemas/responseObjectOnError" /users/{userId}: get: parameters: - "$ref": "#/components/parameters/userIdInPath" responses: '200': description: ユーザーを取得した content: application/json; charset=utf-8: schema: "$ref": "#/components/schemas/responseObject" '400': description: パラメータのフォーマット不正 content: application/json; charset=utf-8: schema: "$ref": "#/components/schemas/responseObjectOnError" '404': description: ユーザーが見つからなかった content: application/json; charset=utf-8: schema: "$ref": "#/components/schemas/responseObjectOnError" delete: parameters: - "$ref": "#/components/parameters/userIdInPath" responses: '200': description: ユーザーを削除した content: application/json; charset=utf-8: schema: "$ref": "#/components/schemas/responseObject" '404': description: ユーザーが見つからなかった content: application/json; charset=utf-8: schema: "$ref": "#/components/schemas/responseObjectOnError" patch: parameters: - "$ref": "#/components/parameters/userIdInPath" requestBody: content: application/json: schema: "$ref": "#/components/schemas/updateCreateUserRequestObject" example: firstName: patch lastName: patch age: 2 responses: '200': description: ユーザーを更新した content: application/json; charset=utf-8: schema: "$ref": "#/components/schemas/responseObject" '404': description: ユーザーが見つからなかった content: application/json; charset=utf-8: schema: "$ref": "#/components/schemas/responseObjectOnError" dredd は、上記の様な openapi の内容を読み込み、paths のそれぞれの内容に対してサーバーにリクエストを送り、そのレスポンスが openapi で定義された内容になっているかをチェックしてくれます。 例えば、/users の get を見て、dredd は、サーバーにこのエンドポイントへリクエストを投げます。 返ってきたレスポンスと、/users の get の responses に定義されている、schema の内容を比較して、定義通りのレスポンスが返ってくるかチェックします。 では、実際に dredd をどうやって使っていくかを書いていきます。 dredd インストール npm install -g dredd dredd の実行 dredd sample-app-api.yml http://localhost:5000/ 第一引数: dredd に読み込ませるapiのドキュメント 第二引数: テストを行うサーバー これだけで、動きます。 しかし、実際に dredd を動かすといろいろ問題が出てくると思います。 ベーシック認証などがサーバーに実装されていると、すべてのテストが Unauthorized で失敗してしまう DELETE /users/{userId} のテストをするときに、削除対象のユーザーがわからない(もしくは、そもそもユーザーが存在しないかもしれない)。また、PATCH /users/{userId} などの、既存のユーザーの存在に依存するエンドポイントで同様に問題になります。 1 については、dredd 実行時のオプションで --header="Authorization: Basic aG9nZTpmdWdh" のように指定してあげることで解決できます。 2 は、リクエストするときに、userId がわからないことが原因で問題になります。そのため、リクエスト時に userId を指定できれば解決できます。 テスト実行の際に、リクエストの内容を変えるための機能として、hooks があります。 hooks を使うことで、柔軟にリクエストの内容を編集したり、レスポンスの期待値を変えたりすることができます。 hooks を使って、リクエスト内容をいじる hooks は dredd に付属している機能なので、dredd をインストールしたらすぐに使えます。 hooks は各種言語(Nodejs ,Go ,Perl ,PHP ,Python ,Ruby ,Rust) で書くことができます。 前述の通り、DELETE で削除するべきユーザーがわからない、もしくは存在しないのが問題なので、dredd がテストを実行する前に、hooks を使って削除されるためだけのユーザーを作成したいです。 このような場合、下記のような hooks を書くことで対応できます。 hooks.js const hooks = require("hooks"); const axios = require("axios"); hooks.before("/users/{userId} > DELETE > 200 > application/json; charset=utf-8", async (transaction, done) => { let deleteRequestPath = ""; const newUser = { firstName: "dummy-user", lastName: "dummy-user", age: 1, }; const response = await axios.post("http://localhost:5000/users", newUser); hooks.log(`テスト実行前に、${response.data.message} (対象: ${transaction.name})`); deleteRequestPath = `/users/${response.data.body.id}`; transaction.fullPath = deleteRequestPath; transaction.request.uri = deleteRequestPath; done(); }); まずは、hooks を読み込みます。 hooks には、before や beforeEach、after afterEach など、テストの実行ライフサイクルに対応する api があり、ライフサイクルのタイミング毎で実行されるコードを定義できます。 上記の場合、before を使って DELETE /users/{userId} がテストされる前に実行するコードをコールバック関数で定義しています。 DELETE /users/{userId} のエンドポイントは、hooks の before の第一引数で下記のように表します。 /users/{userId} > DELETE > 200 > application/json; charset=utf-8 この表し方は、dredd がそれぞれのエンドポイントを識別するために使う API 名で、下記の dredd コマンドで確認できます。 dredd sample-app-api.yml http://localhost:5000/ --names 第二引数のコールバック関数の中身では、テスト対象のサーバーに対して、POST /users を行って、それで作られたユーザーのユーザーIDを利用して、DELETE /users/{userId} の userId の部分を設定しています。 コールバック関数の引数には、dredd がリクエストする内容を表す transaction オブジェクトが渡されます。リクエスト先のパスと、uri をこのコールバック関数内で作成したユーザーIDを設定したものに更新することで、作ったばかりのユーザーに対して、DELETE /users/{userId} が行えます。 最後の done() は、このコールバック関数が非同期でAPIリクエストを実行しているので、必要になります。コールバック関数が非同期の場合、done() が実行されるタイミングで、テストが実行されます。 非同期の処理を行わない場合は、この done() は不要です。 hooks を使うと、こんな漢字で、テストの実行前にデータを用意したり、また、テストが終わるときに、不要なデータを削除といったこともできます。 OAuth などの認証を利用している場合も、hooks を使うことで、対応できると思います。 最後に、hooks を完成させて、テスト実行してみる テストを通すために、下記のような hooks を書いて実行してみます。 hooks.js const hooks = require("hooks"); const axios = require("axios"); // POST /users のリクエストを投げるときに、サーバーは application/json を期待しているので、 // あえて、違う content-type を指定している。 hooks.before("/users > POST > 415 > application/json; charset=utf-8", (transaction) => { transaction.request.headers["Content-Type"] = "application/x-www-form-urlencoded"; }); // GET /users/{userId} のリクエストを投げるとき、{userId} には、英数字とハイフンしか受け付けていないので、 // 対象外の文字が入っている場合、バッドリクエストのエラーを返すようにサーバー側で実装。 // それをテストするために、テスト実行前にパスパラメータには誤った内容を設定している。 hooks.before("/users/{userId} > GET > 400 > application/json; charset=utf-8", (transaction) => { const dummyPathParameter = "/users/@"; transaction.fullPath = dummyPathParameter; transaction.request.uri = dummyPathParameter; }); // 404 のエラーをテストしたいので、存在しないユーザーのIDをパスパラメータにセット hooks.before("/users/{userId} > GET > 404 > application/json; charset=utf-8", (transaction) => { const dummyPathParameter = "/users/1234567890"; transaction.fullPath = dummyPathParameter; transaction.request.uri = dummyPathParameter; }); // ユーザー削除のテストをするために、事前に削除されるだけのユーザーを作成 hooks.before("/users/{userId} > DELETE > 200 > application/json; charset=utf-8", async (transaction, done) => { let userIdToBeDeleted = ""; const newUser = { firstName: "dummy-user", lastName: "dummy-user", age: 1, }; const response = await axios.post("http://localhost:5000/users", newUser); hooks.log(`テスト実行前に、${response.data.message} (対象: ${transaction.name})`); userIdToBeDeleted = `/users/${response.data.body.id}`; transaction.fullPath = userIdToBeDeleted; transaction.request.uri = userIdToBeDeleted; done(); }); // 404 のエラーをテストしたいので、存在しないユーザーのIDをパスパラメータにセット hooks.before("/users/{userId} > DELETE > 404 > application/json; charset=utf-8", (transaction) => { const dummyPathParameter = "/users/1234567890"; transaction.fullPath = dummyPathParameter; transaction.request.uri = dummyPathParameter; }); // ユーザー情報を更新するテストをするために、テスト実行前に、ユーザーを作成してから、そのユーザーに対して更新のテストを行う hooks.before("/users/{userId} > PATCH > 200 > application/json; charset=utf-8", async (transaction, done) => { let userIdToBeDeleted = ""; const newUser = { firstName: "dummy-user", lastName: "dummy-user", age: 2, }; const response = await axios.post("http://localhost:5000/users", newUser); hooks.log(`テスト実行前に、${response.data.message} (対象: ${transaction.name})`); userIdToBeDeleted = `/users/${response.data.body.id}`; transaction.fullPath = userIdToBeDeleted; transaction.request.uri = userIdToBeDeleted; done(); }); // 404 のエラーをテストしたいので、存在しないユーザーのIDをパスパラメータにセット hooks.before("/users/{userId} > PATCH > 404 > application/json; charset=utf-8", (transaction) => { const dummyPathParameter = "/users/1234567890"; transaction.fullPath = dummyPathParameter; transaction.request.uri = dummyPathParameter; }); dredd 実行時に、上記の hooks.js を指定して、実行します。 dredd sample-app-api.yml http://localhost:5000/ --hookfiles=./hooks.js --header="Authorization: Basic aG9nZTpmdWdh" 結果↓ (node:4517) Warning: Accessing non-existent property 'padLevels' of module exports inside circular dependency (Use `node --trace-warnings ...` to show where the warning was created) pass: GET (200) /users duration: 75ms pass: POST (201) /users duration: 15ms pass: POST (415) /users duration: 12ms pass: GET (200) /users/9999999999-999999999-99999999 duration: 12ms pass: GET (400) /users/9999999999-999999999-99999999 duration: 15ms pass: GET (404) /users/9999999999-999999999-99999999 duration: 11ms hook: テスト実行前に、ユーザーID: 375dd003-fc93-4c07-9450-f5c35af8cd44 のユーザーを作成しました (対象: /users/{userId} > DELETE > 200 > application/json; charset=utf-8) pass: DELETE (200) /users/9999999999-999999999-99999999 duration: 15ms pass: DELETE (404) /users/9999999999-999999999-99999999 duration: 12ms hook: テスト実行前に、ユーザーID: 3c492a37-3ae2-4142-8ad8-893ecebb81aa のユーザーを作成しました (対象: /users/{userId} > PATCH > 200 > application/json; charset=utf-8) pass: PATCH (200) /users/9999999999-999999999-99999999 duration: 17ms pass: PATCH (404) /users/9999999999-999999999-99999999 duration: 12ms complete: 10 passing, 0 failing, 0 errors, 0 skipped, 10 total complete: Tests took 342ms 実行されたそれぞれのテストに対して pass と表示されて、openapi で定義されたとおりのレスポンスが返ってきたことが確認できました。 failing の数も 0 なので問題無さそうです! もし、返ってきたレスポンスが、openapi とは異なる内容だった場合は fail と表示されて、テスト実行の詳細が表示されるので、 それをみて、ドキュメントを修正するなり、サーバーを修正するなりして対応していきます。 下記の画像は fail が起きた例です。 idd という項目が openapi に書かれていたのでエラー。正しくは id なので openapi のドキュメントを直すことで pass にできます。 以上です dredd は openapi がすでにあれば、それですぐテスト自動化して、CI等に組み込めそうなので、便利なツールかなと思います。

Viewing all articles
Browse latest Browse all 9134

Trending Articles