kakakakakku blog

Weekly Tech Blog: Keep Learning!

OpenAPI Arazzo ベースの API テストツール Redocly Respect に入門する

最近 OpenAPI 定義の通りに API が実装されているかどうかをテストできる Redocly Respect を検証する機会があった.今まで Redocly では build-docs コマンドや lint コマンドなどを使っていたけど,Redocly Respect は使ったことがなかった.

redocly.com

Redocly Respect は OpenAPI Arazzo(OpenAPI Initiative 主導で作られた API ワークフローのオープン仕様)をベースにした API コントラクトテストツールで,YAML で定義したワークフローに沿って API をテストできる.

spec.openapis.org

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」を試す.

redocly.com

デモ用の 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                                       │ 11       │ -       │ -        │
└─────────────────────────────────────────────────────────────────┴────────────┴─────────┴─────────┴──────────┘

$ 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 定義は同じものを使う.

redocly.com

ちなみに最初に出てくる npx @redocly/cli preview demo.yaml コマンドは間違っていて,エラーになってしまう.正しくは npx @redocly/cli preview で,修正するプルリクエストを送っておいた.既に merge してもらっている❗️

$ npx @redocly/cli preview demo.yaml
Unknown argument: demo.yaml

github.com

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                                       │ 1414      │ -       │ -        │
└────────────────────────────────────────────────────────────────────┴────────────┴─────────┴─────────┴──────────┘

さらにテストが落ちることも確認しておく.OpenAPI 定義の activity.dataname プロパティの型を 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                                       │ 101       │ -        │
└────────────────────────────────────────────────────────────────────┴────────────┴─────────┴─────────┴──────────┘

 Tests exited with error

Test a sequence of API calls with Respect

最後は「Test a sequence of API calls with Respect」を試す.OpenAPI 定義は同じものを使う.

redocly.com

まず 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                                       │ 11       │ -       │ -        │
└──────────────────────────────────────────────────────────────┴────────────┴─────────┴─────────┴──────────┘

まとめ

今までは runn・Postman + Newman・Step CI といったツールを使って API のテストを実装したことがあったけど,今回 Redocly Respect を検証する機会があって勉強になった.特に OpenAPI Arazzo という仕様があるのは知らなくて,今後も動向をウォッチしたいと思う.

redocly.com

関連記事

kakakakakku.hatenablog.com

kakakakakku.hatenablog.com