🐯

Step CIを用いたAPI自動テストのすゝめ

2024/03/21に公開

はじめに

こんにちは、D2Cのエンジニアの吉田です。
本日は私たちのチームが普段 Web API 開発で実際に利用している Step CI を用いたAPI自動テストの構築例を紹介しようと思います。

Step CI の簡単な使い方や利点をまとめ、CI/CDパイプラインとの統合についてまとめます。

Step CI とは

Step CI はオープンソースのAPI自動テストツールです。
言語にとらわれず、YAML形式で簡単にAPIテストを記述することが可能です。

この記事ではRESTベースのAPIテストを紹介しますが、GraphQLgRPCtRPCSOAPといった異なるタイプのAPIテストを1つのワークフローでテストすることができます。
他にもCI/CDに組み込むことも可能で、Github ActionBitbucket Pipelinesと統合することで継続的に自動テストを実行することができます。

また、テスト中に指定したレスポンスの値を名前付き変数で保持することが可能なので、後続の別のAPIテストケースで使用することができます。そのため、登録から参照、更新、削除といった複数のAPIを組み合わせたシナリオ試験にも適しています。

機能が豊富で使いやすいツールですので、ぜひ公式ドキュメントを覗いてみてください!

https://stepci.com/

Step CI を用いたシナリオ試験例

それでは、APIのシナリオ試験を想定し、実際にStep CIを使っていきましょう!
動作の検証環境は以下の通りです。

  • Step CI: v2.8.0

API仕様-Swagger

今回テストしたいAPIの仕様は以下の通りとします。
ログイン、商品登録、そして登録した商品の照会が可能な簡単な仕様とします。
UI表示で見たい場合は以下コードをSwagger Editorに貼り付けてご確認ください。

swagger.yml
openapi: 3.0.1
info:
  version: '1.0'
  title: Test API
servers:
  - url: 'http://localhost:8080'
    description: Test server
paths:
  /api/v1/users/login:
    post:
      tags:
        - Users
      summary: ユーザーログイン
      operationId: post-users-login
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  example: samples@example.com
                password:
                  type: string
                  example: xxxxxxxxx
              required:
                - email
                - password
      responses:
        '201':
          headers:
            Set-Cookie:
              description: セッションID
              schema:
                type: string
              # 試験用の返却値のため設定は最低限
              example: testSessionId=xxxx;path="*"
          description: Successfully logged in
          content:
            application/json:
              schema:
                type: object
                properties:
                  user_id:
                    type: string
                    format: uuid
                    example: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
                required:
                  - user_id
  /api/v1/items:
    post:
      tags:
        - Items
      summary: 商品作成
      operationId: post-items
      parameters:
        - name: testSessionId
          in: cookie
          required: true
          schema:
            type: string
      security:
        - cookieAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  example: チョコレート
                price:
                  type: integer
                  example: 1000
              required:
                - name
                - price
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  item_id:
                    type: string
                    format: uuid
                    example: yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
                required:
                  - item_id
  /api/v1/items/{item_id}:
    get:
      tags: 
        - Items
      summary: 商品取得
      operationId: get-item-by-id
      parameters:
        - name: testSessionId
          in: cookie
          required: true
          schema:
            type: string
        - name: item_id
          in: path
          schema:
            type: string
          required: true
      security:
        - cookieAuth: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  item_id:
                    type: string
                    format: uuid
                    example: yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
                  name:
                    type: string
                    example: チョコレート
                  price:
                    type: integer
                    example: 1000
                required:
                  - item_id
                  - name
                  - price
components: 
  securitySchemes:
    cookieAuth:
      type: apiKey
      in: cookie
      name: testSessionId

上記APIの概要は次の通りです。

  • ユーザーログインAPI POST /api/v1/users/login
    • メールアドレスとパスワードからログインする。
    • ログインに成功すると CookietestSessionIdを取得する。
  • 商品作成API POST /api/v1/items
    • testSessionIdが必須。
    • ログイン済みのユーザーが、商品を作成する。
  • 商品取得API GET /api/v1/items/{item_id}
    • testSessionIdが必須。
    • ログイン済みのユーザーが、商品IDに一致する商品を取得する。

これらのAPIに対して ①ログイン -> ②商品作成 -> ③作成した商品情報の取得 という一連のシナリオ試験を、Step CIを使って作成したいと思います。

シナリオ試験の作成 ① - ログイン

商品操作するにはまずログインが必要となるので、ログインの試験を書いていきます。
なお、事前に公式のGetting startedを参考にstepciをインストールしていることを前提とします。

workflow.ymlを用意して、ログインAPIの試験を記述します。

version: "1.1"
name: Test API
env:
  host: localhost:8080
tests:
  scenario_1:
    name: Login and register items
    steps:
      - id: post-users-login
        name: ログイン
        http:
          # test対象のAPI URL
          url: http://${{env.host}}/api/v1/users/login
          method: POST
          headers:
            Content-Type: application/json
            accept: application/json
          # test時のリクエスト
          json:
            email: samples@example.com
            password: xxxxxxxxx
          # 検証したい内容
          check:
            status: 201
            headers:
              Content-Type: application/json
            schema:
              type: object
              properties:
                user_id:
                  type: string
                  format: uuid
              required:
                - user_id
            jsonpath:
              $.user_id: a2805875-ccb6-34f5-c5bc-1d205363caa9
          captures:
            sessionId:
              cookie: testSessionId

初見でも分かりやすい構成になっているかと思います。
試験したいAPIの情報を記述し、リクエストボディはjson:で指定可能です。
そして、レスポンスでチェックしたい内容をcheck:配下に記述していきます。ここでは以下内容をチェックしていますが、必要に応じてチェック項目を追加したり、削ったりしましょう。

  • レスポンスのHTTPステータスコード
  • Content-Type
  • レスポンスボディのJSONスキーマ
  • レスポンスボディの各フィールド値

また、ログイン成功時のCookieにセットされたセッションIDは、後続のAPI試験でも使用したいので、captures:により、変数名sessionId(任意の名称を指定可能)に保持します。

テストの実行

さて、一度stepciを実行し、テストしてみましょう。
テスト対象となるAPIは、今回は簡略化のため用意したSwaggerからモックサーバーを立て、それに対して試験することにします。
そのため、最初に提示したSwaggerのexampleの値と、テスト期待値を実は既に合わせてあります。

モックサーバーは、実際に私たちのチームで活用しているprismを使おうと思います。
Dockerコマンド一つでswagger.ymlからモックサーバーを起動してくれます。

docker run --rm -it -p 8080:4010 -v $PWD:/mock stoplight/prism:4 mock -h 0.0.0.0 /mock/swagger.yml

やや本題からずれましたが、準備はできたのでstep ciによるテストを実行しましょう。
実行は簡単で、stepci runコマンドを実行するだけでAPIテストを走らせることができます。

$ stepci run ./workflow.yml

実行すると以下のように結果がコンソールに出力され、テストが成功したことがわかります!
モックサーバーからはログイン時の戻り値でuser_id=a2805875-ccb6-34f5-c5bc-1d205363caa9を返却するように設定してあるので、試験期待値と一致し、テストをパスします。

PASS  Login and register items ⏲ 0.97s ⬆ 0 bytes ⬇ 0 bytes

Tests: 0 failed, 1 passed, 1 total
Steps: 0 failed, 0 skipped, 1 passed, 1 total
Time:  0.983s, estimated 1s
CO2:   0.00002g

Workflow passed after 0.983s
Give us your feedback on https://step.ci/feedback

シナリオ試験の作成 ② - 商品の作成と取得

ログインに成功してCookieからセッションIDを取得できるところまで試験できたので、その認証情報を用いて商品を作成し、作成した商品の商品IDを用いて商品情報を取得できるところまでのシナリオ試験を完成させます。

# ----- 追加 -----
      - id: post-items
        name: 商品作成
        http:
          url: http://${{env.host}}/api/v1/items
          method: POST
          cookies:
            testSessionId: ${{captures.sessionId}} # ※1
          headers:
            Content-Type: application/json
            accept: application/json
          json:
            name: チョコレート
            price: 1000
          check:
            status: 201
            schema:
              type: object
              properties:
                item_id:
                  type: string
                  format: uuid
              required:
                - item_id
          captures:
            itemId:
              jsonpath: $.item_id # ※2
      - id: get-item-by-id
        name: 商品取得
        http:
          url: http://${{env.host}}/api/v1/items/${{captures.itemId}}  # ※2
          method: GET
          cookies:
            testSessionId: ${{captures.sessionId}}
          check:
            status: 200
            schema:
              type: object
              properties:
                item_id:
                  type: string
                  format: uuid
                name:
                  type: string
                price:
                  type: integer
              required:
                - item_id
                - name
                - price
            jsonpath:
              $.item_id: ${{captures.itemId}}  # ※2
              $.name: チョコレート
              $.price: 1000
# ----- 追加 -----

ログインの時と記述の流れはほぼ同じですが、ここでのポイントとしては以下2点です。

  • ※1: ログイン時にキャプチャしたセッションIDを${{captures.sessionId}}で繰り返し利用できる点
  • ※2: 商品作成APIのレスポンスで取得した商品IDを、captures.itemIdで保持し、商品取得APIのURLや、レスポンスのチェックに使用できる点

これにより、「①ログイン -> ②商品作成 -> ③作成した商品情報の取得」 のシナリオ試験を作成することができました。
直感的にテストを記述できるので、非常に使いやすいですね!

以下、workflow.yml全文

workflow.yml
version: "1.1"
name: Test API
env:
  host: localhost:8080
tests:
  scenario_1:
    name: Login and register items
    steps:
      - id: post-users-login
        name: ログイン
        http:
          # test対象のAPI URL
          url: http://${{env.host}}/api/v1/users/login
          method: POST
          headers:
            Content-Type: application/json
            accept: application/json
          # test時のリクエスト
          json:
            email: samples@example.com
            password: xxxxxxxxx
          # 検証したい内容
          check:
            status: 201
            headers:
              Content-Type: application/json
            schema:
              type: object
              properties:
                user_id:
                  type: string
                  format: uuid
              required:
                - user_id
            jsonpath:
              $.user_id: a2805875-ccb6-34f5-c5bc-1d205363caa9
          captures:
            sessionId:
              cookie: testSessionId
      - id: post-items
        name: 商品作成
        http:
          url: http://${{env.host}}/api/v1/items
          method: POST
          cookies:
            testSessionId: ${{captures.sessionId}}
          headers:
            Content-Type: application/json
            accept: application/json
          json:
            name: チョコレート
            price: 1000
          check:
            status: 201
            schema:
              type: object
              properties:
                item_id:
                  type: string
                  format: uuid
              required:
                - item_id
          captures:
            itemId:
              jsonpath: $.item_id
      - id: get-item-by-id
        name: 商品取得
        http:
          url: http://${{env.host}}/api/v1/items/${{captures.itemId}}
          method: GET
          cookies:
            testSessionId: ${{captures.sessionId}}
          check:
            status: 200
            schema:
              type: object
              properties:
                item_id:
                  type: string
                  format: uuid
                name:
                  type: string
                price:
                  type: integer
              required:
                - item_id
                - name
                - price
            jsonpath:
              $.item_id: ${{captures.itemId}}
              $.name: チョコレート
              $.price: 1000

Step CI の利点

Step CIを実際に業務でも使用してますが、私が感じている大きな利点は以下の通りです。

  • yaml形式で誰でも簡単にすぐにテストを書くことができるので、学習コストが低い
  • APIのレスポンスの値を変数として保持できるので、API同士のシナリオ試験にも向いている
  • CI/CDに組み込み可能
  • 公式のDockerイメージも用意されているので、すぐに実行可能

上記の他にも色々と機能がありますが、私的に中でも良いと思った機能抜粋です。

  • Matchers: レスポンスのCheckに使用することで、より細かなレスポンス値の評価が可能
  • OpenAPIからのWorkflow生成 : stepci generateコマンドを使えば、swaggerからworkflow.ymlを自動生成することが可能

機能が豊富なので、ぜひ公式ドキュメントをぜひ見てみてください!

実際の運用方法

Bitbucket Pipelines との統合

私たちのチームはBitbucketを利用しているので、Step CIをBitbucket Pipelinesに組み込み、自動テストを実行しています。

Bitbucket Pipelinesでは、bitbucket-pipelines.ymlを用意して、パイプラインで実行する内容を記述します。
以下に示すのは、go言語のAPIアプリケーションに対するStep CIの実行例です。
npm installをステップ上で利用して直接stepciコマンドを実行する方法もありますが、ここではDockerを利用する方式を取ってます。

bitbucket-pipelines.yml

image: golang:1.22

pipelines:
  default:
    - step:
        name: "Unit Test and Build"
        script:
          - echo "Starting go test..."
          - go get
          - go test ./...
          - echo "Starting go build..."
          - go build -o ./main
        artifacts:
          - main
    - step:
        name: "API Test Step CI"
        script:
          - echo "Running API Server..."
          - ./main &
          - echo "Starting Step CI Test..."
          - docker run --add-host host.docker.internal:host-gateway -v "$(pwd)"/tests:/tests ghcr.io/stepci/stepci /tests/workflow.yml -e HOST=host.docker.internal:8080
          - echo "stepci test completed."
        services:
          - docker

あとはPushするだけで、パイプラインが動き、Step CIによる自動テストが実行されます。簡単ですね!

開発時は、先にStep CIのテストを記述して失敗するテストコードを書き、それをグリーンにしてリファクタリングを繰り返すテスト駆動開発で進めることもできます。

詳細な運用方法はチームによりますが、いずれにせよローカル開発や、Github Action、Bitbucket Pipelinesなどに簡単に取り入れることが可能です。

最後に

Step CIいかがだったでしょうか?
オープンソースで、学習コストおよび導入コストが低いにも関わらず、柔軟なテストケースを表現できる点が推しポイントです。

この記事内で紹介できてはいないですが、ここで挙げた機能以外にも使えそうな機能が豊富にありますので、ぜひ一度調べて使ってみてください!

ありがとうございました!

参考

https://docs.stepci.com/
https://github.com/stepci/stepci
https://support.atlassian.com/ja/bitbucket-cloud/docs/run-docker-commands-in-bitbucket-pipelines/

D2C m-tech

Discussion