バックエンドのシナリオテストをOASっぽく書けるrunnに入門してみた

バックエンドのシナリオテストをOASっぽく書けるrunnに入門してみた

2025.10.30

はじめに

バックエンドの開発をしていると、バックエンド単体でシナリオテストを実施したいケースがあります。

PostmanやInsomniaなど様々なツールで実施することができると思いますが、今回はバックエンドのシナリオテストをOpenAPI Specっぽく記述でき、CLIで実行できるrunnというツールが便利だったので入門していきたいと思います。

https://github.com/k1LoW/runn

runbook

runnで実行するテストを記述したyamlファイルはrunbookと呼ばれています。
runbookでは steps で実行手順を記述していきますが、使用するランナー(APIやDBなど)を runners で指定し、 steps では runners のキーを用いてランナーを呼び出します。

例として後述のヘルスチェックのテストで使用するrunbookを見ていきます。

health-check.yml
desc: ヘルスチェックのテスト
runners:
  api: ${API_URL}
steps:
  - desc: サーバーが正常に動作していることを確認する
    api:
      /health:
        get:
          body: null
    test: |
      current.res.status == 200
      && current.res.body.message == "Server is running"

apiというキーでAPIへのリクエストを担うrunnerが指定されています。
apiの接続先のURLは環境変数から取り込むようにしているため、今回のケースでは環境変数を設定する必要があります。

.env
API_URL=http://localhost:3000

こちらのrunbookではapiのrunnerを使用し、/healthエンドポイントに対してGETリクエストを実行しています。
その後、レスポンスに対してステータスコードが200、Bodyに含まれるmessageの値が Server is running であることを評価しています。

使ってみる

実際に使ってみます。
上述のヘルスチェックのrunbookのテストを実行するため以下のようなAPIを準備しました。

paths:
  /health:
    get:
      tags:
        - Health
      summary: ヘルスチェック
      description: サーバーの稼働状態を確認します
      operationId: healthCheck
      responses:
        '200':
          description: サーバーが正常に稼働中
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Server is running

runbookは上述したものをそのまま使えるので、health-check.ymlを runn run コマンドで実行してみます。

runn run ./runbooks/health-check.yml --verbose --env-file=.env

以下のような結果が出力され、無事にサーバーが稼働していることを確認できました。

=== ヘルスチェックのテスト (./runbooks/health-check.yml)
    --- サーバーが正常に動作していることを確認する (0) ... ok

シナリオテスト

せっかくなのでシナリオを作成しテストを実行してみます。
CRUD操作を行えるTodosというエンドポイントを用意しました。
REST準拠のため詳細は割愛しますが、スキーマは以下のようにしています。

components:
  schemas:
    # 一覧取得のスキーマ
    TodosListResponse:
      type: object
      description: TODO一覧レスポンス
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Todo'
          description: TODOの配列
        total:
          type: integer
          example: 5
          description: TODOの総数

    # todo本体のスキーマ
    Todo:
      type: object
      properties:
        id:
          type: string
          example: '1'
          description: TODO ID
        title:
          type: string
          example: タイトル
          description: TODOのタイトル
        description:
          type: string
          example: 説明
          description: TODOの説明

runbookは以下のものを用意しました。

todos-crud.yml
desc: Todos APIのCRUD操作テスト
runners:
  api: ${API_URL}

steps:
  - desc: TODOを新規作成
    api:
      /api/todos:
        post:
          body:
            application/json:
              title: "テストTodo"
              description: "テスト用の新規作成Todo"
    test: |
      // ステータスコードが201であること
      current.res.status == 201
      // レスポンスのBodyに含まれるtitleの値が "テストTodo" であること
      && current.res.body.title == "テストTodo"
      // レスポンスのBodyに含まれるidの値が空でないこと
      && current.res.body.id != ""
    dump:
      current.res.body
    bind:
      todoId: current.res.body.id

  - desc: TODOの一覧を取得し作成したTODOが含まれることを確認
    api:
      /api/todos:
        get:
          body: null
    test: |
      // ステータスコードが200であること
      current.res.status == 200
      // レスポンスのBodyに含まれるitemsの配列の要素数が0以上であること
      && len(current.res.body.items) >= 0
      // レスポンスのBodyに含まれるtotalの値が0以上であること
      && current.res.body.total >= 0
      // レスポンスのBodyに含まれるitemsの配列に作成したTODOのIDが含まれること
      && one(current.res.body.items, .id == todoId) == true

  - desc: 作成したTODOを更新
    api:
      /api/todos/{{ todoId }}:
        put:
          body:
            application/json:
              title: "更新したTodo"
              description: "更新した説明"
    test: |
      // ステータスコードが200であること
      current.res.status == 200
      // レスポンスのBodyに含まれるidの値が作成したTODOのIDと一致すること
      && current.res.body.id == todoId
      // レスポンスのBodyに含まれるtitleの値が "更新したTodo" であること
      && current.res.body.title == "更新したTodo"
      // レスポンスのBodyに含まれるdescriptionの値が "更新した説明" であること
      && current.res.body.description == "更新した説明"
    dump:
      current.res.body

  - desc: TODOの詳細を取得し更新した内容が反映されていることを確認
    api:
      /api/todos/{{ todoId }}:
        get:
          body: null
    test: |
      // ステータスコードが200であること
      current.res.status == 200
      // レスポンスのBodyに含まれるidの値が作成したTODOのIDと一致すること
      && current.res.body.id == todoId
      // レスポンスのBodyに含まれるtitleの値が "更新したTodo" であること
      && current.res.body.title == "更新したTodo"
      // レスポンスのBodyに含まれるdescriptionの値が "更新した説明" であること
      && current.res.body.description == "更新した説明"

  - desc: 作成したTODOを削除
    api:
      /api/todos/{{ todoId }}:
        delete:
          body: null
    test: |
      // ステータスコードが200であること
      current.res.status == 200

  - desc: 削除したTODOの詳細を取得し404エラーが返ってくることを確認
    api:
      /api/todos/{{ todoId }}:
        get:
          body: null
    test: |
      // ステータスコードが404であること
      current.res.status == 404

実行結果は以下のとおりです。

=== Todos APIのCRUD操作テスト (./runbooks/todos_crud.yml)
{
  "description": "テスト用の新規作成Todo",
  "id": "a59e4185-29a6-42f8-a9d0-1a5e3d0dcfa1",
  "title": "テストTodo"
}
    --- TODOを新規作成 (0) ... ok
    --- TODOの一覧を取得し作成したTODOが含まれることを確認 (1) ... ok
{
  "description": "更新した説明",
  "id": "a59e4185-29a6-42f8-a9d0-1a5e3d0dcfa1",
  "title": "更新したTodo"
}
    --- 作成したTODOを更新 (2) ... ok
    --- TODOの詳細を取得し更新した内容が反映されていることを確認 (3) ... ok
    --- 作成したTODOを削除 (4) ... ok
    --- 削除したTODOの詳細を取得し404エラーが返ってくることを確認 (5) ... ok

今回使用したrunbookではいくつか先述のヘルスチェックのテストでは使用していなかった記述をしています。
よく使う記述だと思うので紹介していこうと思います。

dump

TODOの新規作成や更新時にdumpという記述があります。

dump:
  current.res.body

これは指定した値や文字列を出力するために使用します。
runbookでは主にレスポンスの値が期待通りかをテストとして記述していくと思いますが、テストがFailした場合のレスポンスの確認などに役立ちます。
実行ログで表示されている以下のような箇所はdumpによって出力されたものです。

{
  "description": "テスト用の新規作成Todo",
  "id": "a59e4185-29a6-42f8-a9d0-1a5e3d0dcfa1",
  "title": "テストTodo"
}

別名の設定

TODOの新規作成時、以下のような記述をしています。

bind:
  todoId: current.res.body.id

これは指定した値に別名を設定するための記述です。
今回のケースではTODOの新規作成のレスポンスに含まれるIDを以降のテストで使用するため、 todoId という別名をつけることで参照しやすくしています。
別名を設定しなくても steps[0].res.body.id のように記述することで同値を参照することができますが、簡潔で用途を理解しやすいのでよく使う記述だと思います。

別名をつけた値は更新時のパスパラメータや取得・更新時の評価に使用しています。

- desc: 作成したTODOを更新
  api:
    /api/todos/{{ todoId }}:    # ここで使用

...

評価式

TODOの一覧を取得し、新規作成されたTODOが含まれているかを評価している箇所で以下のような記述があります。

test: |
  ...
  // レスポンスのBodyに含まれるitemsの配列に作成したTODOのIDが含まれること
  && one(current.res.body.items, .id == todoId) == true

これは配列に対し、一つだけ条件に合致する要素がある場合trueを返却するという評価式です。
runnでは評価式でexpr-lang/exprに基づいた記述をすることができます。

文字列をスプリットしたり、配列に対して全ての要素が条件を満たすかを確認したりすることができ、期待値の評価でよく使うと思うので確認しながらrunbookを作成すると良いでしょう。

https://expr-lang.org/docs/language-definition

まとめ

今回は入門でしたが、他にも別シナリオの参照、依存関係の設定、リクエスト内容の外部ファイルへの切り出しなど実用的な機能がかなり揃っている印象で、もっと深く理解し使いこなしていきたいと感じました。
GitHub Actionsで使用することもできるようで、CIに組み込むなどプロダクトの品質管理に大きく貢献しそうだなと思いました。
何よりyamlで記述でき、記述もシンプルで手軽というのが素晴らしく、これからも継続的に利用しおすすめしていきたいなと思うツールです。
使っていく中で便利だと感じた機能についてはまた記事にしていこうと思います。

この記事をシェアする

FacebookHatena blogX

関連記事