
最近 OpenAPI 定義の通りに API が実装されているかどうかをテストできる Redocly Respect を検証する機会があった.今まで Redocly では build-docs コマンドや lint コマンドなどを使っていたけど,Redocly Respect は使ったことがなかった.
Redocly Respect は OpenAPI Arazzo(OpenAPI Initiative 主導で作られた API ワークフローのオープン仕様)をベースにした API コントラクトテストツールで,YAML で定義したワークフローに沿って API をテストできる.
Redocly Respect に入門するためにドキュメントにあるコンテンツ3種類をやってみた💪
- Get started with Respect
- Generate and run API tests with Respect
- Test a sequence of API calls with Respect
Get started with Respect
まずはドキュメントにある「Get started with Respect」を試す.
デモ用の OpenAPI 定義をダウンロードする.Learning API Demo という架空の API で,クイズ・課題・チェックリストなど学習管理システムに必要な操作がまとまっている.
$ curl https://api.redocly.com/registry/bundle/testing_acme/training/v1/openapi.yaml > demo.yaml
次に users-test1.arazzo.yaml ファイルを作る.Arazzo ファイルの steps を見るとわかる通り,listUsers(ユーザー一覧取得) → getOneUser(個別ユーザー取得)という2つの API を呼び出すワークフローになっている.また listUsers で取得したユーザー配列の1番目(インデックス0)を getOneUser に渡すようになっている.
arazzo: 1.0.1 info: title: Demo Arazzo and Respect version: 1.0.0 sourceDescriptions: - name: demo type: openapi url: demo.yaml workflows: - workflowId: listAndFetchUser inputs: type: object properties: env: type: object properties: IMFKEY: type: string format: password # this scrubs the value from logs parameters: - in: header name: IMF-KEY value: $inputs.IMFKEY steps: - stepId: listUsers operationId: demo.GetUserList outputs: id: $response.body#/0/id - stepId: getOneUser operationId: demo.GetUserActivity parameters: - name: id in: path value: $steps.listUsers.outputs.id
あとは Redocly の respect コマンドで Arazzo ファイルを実行するとテストが通った❗️ちなみに --verbose オプションを付けると HTTP リクエストと HTTP レスポンスまで詳細にログ出力される.
$ npx @redocly/cli respect users-test1.arazzo.yaml --input IMFKEY=abc Running workflow users-test1.arazzo.yaml / listAndFetchUser ✓ GET /users - step listUsers ✓ status code check - $statusCode in [200, 400] ✓ content-type check ✓ schema check ✓ GET /users/{id} - step getOneUser ✓ status code check - $statusCode in [200, 400] ✓ content-type check ✓ schema check Summary for users-test1.arazzo.yaml Workflows: 1 passed, 1 total Steps: 2 passed, 2 total Checks: 6 passed, 6 total Time: 1294ms ┌─────────────────────────────────────────────────────────────────┬────────────┬─────────┬─────────┬──────────┐ │ Filename │ Workflows │ Passed │ Failed │ Warnings │ ├─────────────────────────────────────────────────────────────────┼────────────┼─────────┼─────────┼──────────┤ │ ✓ users-test1.arazzo.yaml │ 1 │ 1 │ - │ - │ └─────────────────────────────────────────────────────────────────┴────────────┴─────────┴─────────┴──────────┘ $ npx @redocly/cli respect users-test1.arazzo.yaml --input IMFKEY=abc --verbose (割愛)
ということで「Get started with Respect」を試すと Arazzo ファイルと Redocly の respect コマンドの基本的なイメージを掴める👌
Generate and run API tests with Respect
次に「Generate and run API tests with Respect」を試す.OpenAPI 定義は同じものを使う.
ちなみに最初に出てくる npx @redocly/cli preview demo.yaml コマンドは間違っていて,エラーになってしまう.正しくは npx @redocly/cli preview で,修正するプルリクエストを送っておいた.既に merge してもらっている❗️
$ npx @redocly/cli preview demo.yaml Unknown argument: demo.yaml
1つ前の「Get started with Respect」は Arazzo ファイルをコピーして作ったけど,Redocly の generate-arazzo コマンドで自動生成できる.実行すると auto-generated.arazzo.yaml ファイルが生成された.
$ npx @redocly/cli generate-arazzo demo.yaml Generating Arazzo description... Arazzo description auto-generated.arazzo.yaml successfully generated.
ちょっと長くなるけど,生成された auto-generated.arazzo.yaml をそのまま貼っておく📝自動生成される Arazzo ファイルは,それぞれの API エンドポイントを単独で呼び出すワークフローになっている.
- get-workflow
- post-activities-workflow
- post-quizzes-workflow
- get-quizzes-workflow
- post-checklists-workflow
- get-checklists-workflow
- post-badges-workflow
- get-badges-workflow
- post-assignments-workflow
- get-assignments-workflow
- get-scores-workflow
- get-users-workflow
- get-users-{id}-workflow
- get-reports-summaries-workflow
arazzo: 1.0.1 info: title: Learning API Demo version: v1 sourceDescriptions: - name: demo type: openapi url: demo.yaml workflows: - workflowId: get-workflow inputs: $ref: '#/components/inputs/imfKey' parameters: - name: IMF-KEY value: $inputs.imfKey in: header steps: - stepId: get-step operationId: $sourceDescriptions.demo.GetRoot successCriteria: - condition: $statusCode == 200 - workflowId: post-activities-workflow inputs: $ref: '#/components/inputs/imfKey' parameters: - name: IMF-KEY value: $inputs.imfKey in: header steps: - stepId: post-activities-step operationId: $sourceDescriptions.demo.PostActivity successCriteria: - condition: $statusCode == 204 - workflowId: post-quizzes-workflow inputs: $ref: '#/components/inputs/imfKey' parameters: - name: IMF-KEY value: $inputs.imfKey in: header steps: - stepId: post-quizzes-step operationId: $sourceDescriptions.demo.PostQuiz successCriteria: - condition: $statusCode == 201 - workflowId: get-quizzes-workflow inputs: $ref: '#/components/inputs/imfKey' parameters: - name: IMF-KEY value: $inputs.imfKey in: header steps: - stepId: get-quizzes-step operationId: $sourceDescriptions.demo.GetQuizzes successCriteria: - condition: $statusCode == 200 - workflowId: post-checklists-workflow inputs: $ref: '#/components/inputs/imfKey' parameters: - name: IMF-KEY value: $inputs.imfKey in: header steps: - stepId: post-checklists-step operationId: $sourceDescriptions.demo.PostChecklist successCriteria: - condition: $statusCode == 201 - workflowId: get-checklists-workflow inputs: $ref: '#/components/inputs/imfKey' parameters: - name: IMF-KEY value: $inputs.imfKey in: header steps: - stepId: get-checklists-step operationId: $sourceDescriptions.demo.GetChecklists successCriteria: - condition: $statusCode == 200 - workflowId: post-badges-workflow inputs: $ref: '#/components/inputs/imfKey' parameters: - name: IMF-KEY value: $inputs.imfKey in: header steps: - stepId: post-badges-step operationId: $sourceDescriptions.demo.PostBadge successCriteria: - condition: $statusCode == 201 - workflowId: get-badges-workflow inputs: $ref: '#/components/inputs/imfKey' parameters: - name: IMF-KEY value: $inputs.imfKey in: header steps: - stepId: get-badges-step operationId: $sourceDescriptions.demo.GetBadges successCriteria: - condition: $statusCode == 200 - workflowId: post-assignments-workflow inputs: $ref: '#/components/inputs/imfKey' parameters: - name: IMF-KEY value: $inputs.imfKey in: header steps: - stepId: post-assignments-step operationId: $sourceDescriptions.demo.PostAssignment successCriteria: - condition: $statusCode == 201 - workflowId: get-assignments-workflow inputs: $ref: '#/components/inputs/imfKey' parameters: - name: IMF-KEY value: $inputs.imfKey in: header steps: - stepId: get-assignments-step operationId: $sourceDescriptions.demo.GetAssignments successCriteria: - condition: $statusCode == 200 - workflowId: get-scores-workflow inputs: $ref: '#/components/inputs/imfKey' parameters: - name: IMF-KEY value: $inputs.imfKey in: header steps: - stepId: get-scores-step operationId: $sourceDescriptions.demo.GetScores successCriteria: - condition: $statusCode == 200 - workflowId: get-users-workflow inputs: $ref: '#/components/inputs/imfKey' parameters: - name: IMF-KEY value: $inputs.imfKey in: header steps: - stepId: get-users-step operationId: $sourceDescriptions.demo.GetUserList successCriteria: - condition: $statusCode == 200 - workflowId: get-users-{id}-workflow inputs: $ref: '#/components/inputs/imfKey' parameters: - name: IMF-KEY value: $inputs.imfKey in: header steps: - stepId: get-users-{id}-step operationId: $sourceDescriptions.demo.GetUserActivity successCriteria: - condition: $statusCode == 200 - workflowId: get-reports-summaries-workflow inputs: $ref: '#/components/inputs/imfKey' parameters: - name: IMF-KEY value: $inputs.imfKey in: header steps: - stepId: get-reports-summaries-step operationId: $sourceDescriptions.demo.GetSummaryReport successCriteria: - condition: $statusCode == 200 components: inputs: imfKey: type: object properties: imfKey: type: string description: Authentication token for imfKey format: password
あとは同じように Redocly の respect コマンドで Arazzo ファイルを実行するとテストが通った❗️
ちなみにドキュメントに書いてあるコマンドのファイル名 auto-generated.yaml も間違っていて,エラーになってしまう.正しくは auto-generated.arazzo.yaml で,先ほどと同じプルリクエストで修正しておいた.既に merge してもらっている❗️
$ npx @redocly/cli respect auto-generated.arazzo.yaml ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Running workflow auto-generated.arazzo.yaml / get-workflow ✓ GET / - step get-step ✓ success criteria check - $statusCode == 200 ✓ status code check - $statusCode in [200, 400] ✓ content-type check ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Running workflow auto-generated.arazzo.yaml / post-activities-workflow ✓ POST /activities - step post-activities-step ✓ success criteria check - $statusCode == 204 ✓ status code check - $statusCode in [204, 400] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Running workflow auto-generated.arazzo.yaml / post-quizzes-workflow ✓ POST /quizzes - step post-quizzes-step ✓ success criteria check - $statusCode == 201 ✓ status code check - $statusCode in [201, 400] ✓ content-type check ✓ schema check ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Running workflow auto-generated.arazzo.yaml / get-quizzes-workflow ✓ GET /quizzes - step get-quizzes-step ✓ success criteria check - $statusCode == 200 ✓ status code check - $statusCode in [200, 400] ✓ content-type check ✓ schema check ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Running workflow auto-generated.arazzo.yaml / post-checklists-workflow ✓ POST /checklists - step post-checklists-step ✓ success criteria check - $statusCode == 201 ✓ status code check - $statusCode in [201, 400] ✓ content-type check ✓ schema check ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Running workflow auto-generated.arazzo.yaml / get-checklists-workflow ✓ GET /checklists - step get-checklists-step ✓ success criteria check - $statusCode == 200 ✓ status code check - $statusCode in [200, 400] ✓ content-type check ✓ schema check ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Running workflow auto-generated.arazzo.yaml / post-badges-workflow ✓ POST /badges - step post-badges-step ✓ success criteria check - $statusCode == 201 ✓ status code check - $statusCode in [201, 400] ✓ content-type check ✓ schema check ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Running workflow auto-generated.arazzo.yaml / get-badges-workflow ✓ GET /badges - step get-badges-step ✓ success criteria check - $statusCode == 200 ✓ status code check - $statusCode in [200, 400] ✓ content-type check ✓ schema check ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Running workflow auto-generated.arazzo.yaml / post-assignments-workflow ✓ POST /assignments - step post-assignments-step ✓ success criteria check - $statusCode == 201 ✓ status code check - $statusCode in [201, 400] ✓ content-type check ✓ schema check ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Running workflow auto-generated.arazzo.yaml / get-assignments-workflow ✓ GET /assignments - step get-assignments-step ✓ success criteria check - $statusCode == 200 ✓ status code check - $statusCode in [200, 400] ✓ content-type check ✓ schema check ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Running workflow auto-generated.arazzo.yaml / get-scores-workflow ✓ GET /scores - step get-scores-step ✓ success criteria check - $statusCode == 200 ✓ status code check - $statusCode in [200, 400] ✓ content-type check ✓ schema check ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Running workflow auto-generated.arazzo.yaml / get-users-workflow ✓ GET /users - step get-users-step ✓ success criteria check - $statusCode == 200 ✓ status code check - $statusCode in [200, 400] ✓ content-type check ✓ schema check ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Running workflow auto-generated.arazzo.yaml / get-users-{id}-workflow ✓ GET /users/{id} - step get-users-{id}-step ✓ success criteria check - $statusCode == 200 ✓ status code check - $statusCode in [200, 400] ✓ content-type check ✓ schema check ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Running workflow auto-generated.arazzo.yaml / get-reports-summaries-workflow ✓ GET /reports/summaries - step get-reports-summaries-step ✓ success criteria check - $statusCode == 200 ✓ status code check - $statusCode in [200, 400] ✓ content-type check ✓ schema check Summary for auto-generated.arazzo.yaml Workflows: 14 passed, 14 total Steps: 14 passed, 14 total Checks: 53 passed, 53 total Time: 8608ms ┌────────────────────────────────────────────────────────────────────┬────────────┬─────────┬─────────┬──────────┐ │ Filename │ Workflows │ Passed │ Failed │ Warnings │ ├────────────────────────────────────────────────────────────────────┼────────────┼─────────┼─────────┼──────────┤ │ ✓ auto-generated.arazzo.yaml │ 14 │ 14 │ - │ - │ └────────────────────────────────────────────────────────────────────┴────────────┴─────────┴─────────┴──────────┘
さらにテストが落ちることも確認しておく.OpenAPI 定義の activity.data で name プロパティの型を string から integer に変える.そして --workflow オプションを指定して特定のワークフローに限定して実行する.
すると type must be integer というエラーが出てテストがちゃんと落ちた👌
$ npx @redocly/cli respect auto-generated.arazzo.yaml \ --workflow get-users-{id}-workflow ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Running workflow auto-generated.arazzo.yaml / get-users-{id}-workflow ✗ GET /users/{id} - step get-users-{id}-step ✓ success criteria check - $statusCode == 200 ✓ status code check - $statusCode in [200, 400] ✓ content-type check ✗ schema check Failed tests info: Workflow name: get-users-{id}-workflow stepId - get-users-{id}-step ✗ schema check TYPE must be integer 39 | "data": { 40 | "type": "quiz", > 41 | "name": "Onboarding Part 1", | ^^^^^^^^^^^^^^^^^^^ 👈🏽 type must be integer 42 | "item": "Sign agreements", 43 | "answer": "a" 44 | }, Summary for auto-generated.arazzo.yaml Workflows: 1 failed, 1 total Steps: 1 failed, 1 total Checks: 3 passed, 1 failed, 4 total Time: 772ms ┌────────────────────────────────────────────────────────────────────┬────────────┬─────────┬─────────┬──────────┐ │ Filename │ Workflows │ Passed │ Failed │ Warnings │ ├────────────────────────────────────────────────────────────────────┼────────────┼─────────┼─────────┼──────────┤ │ x auto-generated.arazzo.yaml │ 1 │ 0 │ 1 │ - │ └────────────────────────────────────────────────────────────────────┴────────────┴─────────┴─────────┴──────────┘ Tests exited with error
Test a sequence of API calls with Respect
最後は「Test a sequence of API calls with Respect」を試す.OpenAPI 定義は同じものを使う.
まず sequence.arazzo.yaml ファイルを作る.「Get started with Respect」と似ているけど,POST リクエストを実行して(クイズ登録),取得した ID を次の GET リクエストに使うという流れになっている.実際のアプリケーションではいくつかの API を順番に実行していくようなシナリオもあるため,テスト観点に沿ってワークフローを実装していくことになりそう.
arazzo: 1.0.1 info: title: Learning API Demo version: v1 sourceDescriptions: - name: demo type: openapi url: demo.yaml workflows: - workflowId: sequence-demo steps: - stepId: createQuiz operationId: PostQuiz outputs: quizId: $response.body#/id - stepId: getScores operationId: GetScores parameters: - in: header name: quiz value: $steps.createQuiz.outputs.quizId parameters: - in: header name: IMF-KEY value: $inputs.IMFKEY inputs: type: object properties: env: type: object properties: IMFKEY: type: string format: password
同じように Redocly の respect コマンドで Arazzo ファイルを実行するとテストが通った❗️
$ npx @redocly/cli respect sequence.arazzo.yaml --input IMFKEY=abc ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Running workflow sequence.arazzo.yaml / sequence-demo ✓ POST /quizzes - step createQuiz ✓ status code check - $statusCode in [201, 400] ✓ content-type check ✓ schema check ✓ GET /scores - step getScores ✓ status code check - $statusCode in [200, 400] ✓ content-type check ✓ schema check Summary for sequence.arazzo.yaml Workflows: 1 passed, 1 total Steps: 2 passed, 2 total Checks: 6 passed, 6 total Time: 1579ms ┌──────────────────────────────────────────────────────────────┬────────────┬─────────┬─────────┬──────────┐ │ Filename │ Workflows │ Passed │ Failed │ Warnings │ ├──────────────────────────────────────────────────────────────┼────────────┼─────────┼─────────┼──────────┤ │ ✓ sequence.arazzo.yaml │ 1 │ 1 │ - │ - │ └──────────────────────────────────────────────────────────────┴────────────┴─────────┴─────────┴──────────┘
まとめ
今までは runn・Postman + Newman・Step CI といったツールを使って API のテストを実装したことがあったけど,今回 Redocly Respect を検証する機会があって勉強になった.特に OpenAPI Arazzo という仕様があるのは知らなくて,今後も動向をウォッチしたいと思う.