第2章:シナリオ記述編

runnでは、YAMLを使ってテストシナリオを記述します。プログラミング知識がなくても、チーム全員が読み書きできるテストが作成できます。

Runbook - あなたのテストの台本

runnでは、テストシナリオをRunbookと呼びます。何をどの順番で実行するかを記述したものです。

Runbookの利点

従来のテストコード:

const response = await fetch('/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Alice' })
});
assert(response.status === 201);

runnのRunbook:

desc: シナリオの説明            # このシナリオが何をテストするか

labels:                      # シナリオの分類ラベル(フィルタリングに利用)
  - api
  - user

runners:                     # 使用するランナーの定義
  blog: http://localhost:8080

vars:                        # シナリオで使用する変数
  baseURL: http://localhost:8080
  timeout: 30

steps:                       # 実行するステップの定義
  - blog:
      /users:
        get: {}
    test: current.res.status == 200

YAMLで記述することで、より直感的にテストを表現できます。

Runbookの5つの要素

Runbookは5つの主要セクションで構成されています。

1. desc - シナリオの目的

テストの目的を明確に記述します。

2. labels - テストの分類

--labelオプションで特定のテストのみを実行できます。

3. runners - 接続先の定義

HTTP、gRPC、データベースなどの接続先を定義します。

4. vars - 変数定義

繰り返し使う値を変数として定義できます。

5. steps - 実行ステップ

テストの本体となる、順番に実行されるアクションのリストです。

2つの記述スタイル

runnは2つの記述スタイルを提供しています。

スタイル1: リスト形式

インデックスでステップを参照する、シンプルな方法です。

desc: ユーザー作成とログインのテスト(リスト形式)

runners:
  blog: http://localhost:8080

steps:
  - blog:                              # steps[0]
      /users:
        post:
          body:
            application/json:
              name: "Alice"
              email: "alice@example.com"
    test: steps[0].res.status == 201

  - blog:                              # steps[1]
      /login:
        post:
          body:
            application/json:
              email: "{{ steps[0].res.body.email }}"
              password: "password123"
    test: |
      steps[1].res.status == 200 &&
      steps[1].res.body.token != null

メリット:

  • 学習が簡単
  • 短いシナリオに適している
  • steps[0]steps[1]と直感的に参照できる

スタイル2: マップ形式

名前付きステップで、より読みやすくなります。

desc: ユーザー作成とログインのテスト(マップ形式)

runners:
  blog: http://localhost:8080

steps:
  create_user:                        # 名前付きステップ
    blog:
      /users:
        post:
          body:
            application/json:
              name: "Alice"
              email: "alice@example.com"
    test: steps.create_user.res.status == 201

  login_user:                         # 名前付きステップ
    blog:
      /login:
        post:
          body:
            application/json:
              email: "{{ steps.create_user.res.body.email }}"
              password: "password123"
    test: |
      steps.login_user.res.status == 200 &&
      steps.login_user.res.body.token != null

メリット:

  • ステップの意図が明確
  • 長いシナリオでも管理しやすい
  • steps.create_userのように意味のある名前で参照できる

10ステップ以上のシナリオでは、マップ形式の使用をお勧めします。

変数の活用

変数定義

runnの変数システムを使用すると、柔軟な設定が可能です。

vars:
  # 静的な値
  apiVersion: v1
  timeout: 30

  # 環境変数から取得
  apiKey: ${API_KEY:-test-api-key}
  environment: ${ENV:-development}  # デフォルト値付き

  # 複雑なデータ構造
  testUser:
    name: "Test User"
    email: "test@localhost"
    roles:
      - admin
      - user

runners:
  httpbin: http://localhost:8080

steps:
  test_vars:
    httpbin:
      /anything:
        get:
          headers:
            X-API-Key: "{{ vars.apiKey }}"
    test: current.res.status == 200

  show_vars:
    desc: "変数の値を表示"
    # 変数の値をデバッグ出力
    dump: vars.apiKey

ポイント:

  • 環境変数から自動取得(${API_KEY}
  • デフォルト値の設定(${ENV:-development}
  • 複雑なデータ構造も定義可能

変数参照

定義した変数は以下のように参照できます。

runners:
  httpbin: http://localhost:8080

vars:
  apiVersion: "v1"
  apiKey: "test-api-key"
  timeout: "30"

steps:
  - httpbin:
      /get?v={{ vars.apiVersion }}:  # パス内での変数展開
        get:
          headers:
            X-API-Key: "{{ vars.apiKey }}"
            X-Timeout: "{{ vars.timeout }}"
    test: |
      current.res.status == 200

{{ vars.変数名 }}の記法で変数を参照できます。

ステップ記述の詳細

HTTPリクエストの例

HTTPリクエストの各種機能を使用した例です。

runners:
  blog: http://localhost:8080

vars:
  environment: "test"
  userId: "123"
  token: "test-token"

steps:
  # まずユーザーを作成
  create_user:
    blog:
      /users:
        post:
          body:
            application/json:
              name: "Test User"
              email: "test@localhost"
    test: current.res.status == 201

  api_call:
    desc: ユーザー情報を更新する    # ステップの説明
    if: vars.environment == "test"  # 条件付き実行
    blog:
      /users/{{ steps.create_user.res.body.id }}:
        put:
          headers:
            Content-Type: application/json
            Authorization: "Bearer {{ vars.token }}"
          body:
            application/json:
              name: "Updated Name"
              email: "updated@localhost"
          timeout: 10s              # タイムアウト設定
    test: |                        # テストアサーション
      current.res.status == 200 &&
      current.res.body.name == "Updated Name"
    dump: current.res                # デバッグ用の値出力

標準出力:

{
  "body": {
    "email": "updated@localhost",
    "id": 1,
    "name": "Updated Name"
  },
  "cookies": {},
  "headers": {
    "Content-Length": [
      "59"
    ],
    "Content-Type": [
      "application/json"
    ],
    "Date": [
      "Sun, 27 Jul 2025 23:30:09 GMT"
    ]
  },
  "rawBody": "{\"id\":1,\"name\":\"Updated Name\",\"email\":\"updated@localhost\"}\n",
  "status": 200
}

利用可能な機能:

  • 条件付き実行(if
  • タイムアウト設定
  • テストアサーション
  • デバッグ用のダンプ機能

データベースクエリ

SQLクエリもYAMLで記述できます。

# TODO: Run this example in a test database
runners:
  testdb: sqlite:///tmp/runn-test.db

vars:
  testEmail: "alice@example.com"

steps:
  setup-db:
    db:
      testdb:
        query: |
          CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            email TEXT NOT NULL UNIQUE,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
          );

  insert-user:
    db:
      testdb:
        query: |
          INSERT INTO users (name, email)
            VALUES ('Alice', '{{ vars.testEmail }}')

  check-user:
    db:
      testdb:
        query: |
          SELECT id, name, email, created_at
          FROM users
          WHERE email = $1
        params:
          - "{{ vars.testEmail }}"
    test: |
      len(steps.check_user.rows) == 1 &&
      steps.check_user.rows[0].name == "Alice"

HTTPもデータベースも、同じtest構文でアサーションを記述できます。

実践的なシナリオ例

CRUD操作の実装

完全なCRUD操作を実装した例です。

desc: ブログ記事のCRUD操作をテスト
runners:
  blog: http://localhost:8080/api

vars:
  authorId: "author-123"

steps:
  # 1. 記事を作成
  create_post:
    blog:
      /posts:
        post:
          body:
            application/json:
              title: "テスト記事"
              content: "これはテスト記事です"
              authorId: "{{ vars.authorId }}"
    test: |
      steps.create_post.res.status == 201 &&
      steps.create_post.res.body.id != null

  # 2. 作成した記事を取得
  get_post:
    blog:
      /posts/{{ steps.create_post.res.body.id }}:
        get: {}
    test: |
      steps.get_post.res.status == 200 &&
      steps.get_post.res.body.title == "テスト記事"

  # 3. 記事を更新
  update_post:
    blog:
      /posts/{{ steps.create_post.res.body.id }}:
        put:
          body:
            application/json:
              title: "更新されたテスト記事"
              content: "内容も更新しました"
    test: steps.update_post.res.status == 200

  # 4. 更新を確認
  verify_update:
    blog:
      /posts/{{ steps.create_post.res.body.id }}:
        get: {}
    test: |
      steps.verify_update.res.body.title == "更新されたテスト記事"

  # 5. 記事を削除
  delete_post:
    blog:
      /posts/{{ steps.create_post.res.body.id }}:
        delete: {}
    test: steps.delete_post.res.status == 204

  # 6. 削除を確認
  verify_delete:
    blog:
      /posts/{{ steps.create_post.res.body.id }}:
        get: {}
    test: steps.verify_delete.res.status == 404
1 scenario, 0 skipped, 0 failures

この例では以下を実装しています:

  • Create → Read → Update → Delete のフロー
  • ステップ間でのID受け渡し
  • 削除後の404確認

YAML記述のテクニック

テクニック1: 複数行文字列

長いテキストの記述方法です。

runners:
  httpbin: http://localhost:8080

steps:
  - desc: |
      これは複数行の
      説明文です
    httpbin:
      /post:
        post:
          body:
            text/plain: |
              複数行の
              テキストデータ

テクニック2: アンカー&エイリアス

共通設定を再利用する方法です。

# 共通のヘッダーを定義
commonHeaders: &headers
  Content-Type: application/json
  X-API-Version: "1.0"

runners:
  blog: http://localhost:8080

steps:
  - blog:
      /users:
        get:
          headers:
            <<: *headers          # 共通ヘッダーを使用
            Authorization: "Bearer token123"

共通設定を一箇所で管理することで、メンテナンスが容易になります。

テクニック3: 環境別設定

開発・本番環境の設定を切り替える方法です。

vars:
  baseURL: ${BASE_URL:-http://localhost:8080}
  apiKey: ${API_KEY:-test-api-key}
  environment: ${ENV:-development}

  # 環境別の設定をマップで管理
  config:
    development:
      timeout: 60
      retries: 3
    production:
      timeout: 30
      retries: 1

runners:
  blog: "{{ vars.baseURL }}"

steps:
  test_request:
    blog:
      /test:
        get:
          headers:
            X-API-Key: "{{ vars.apiKey }}"
    test: current.res.status == 200