第2章:シナリオ記述編

「YAMLでテストを書く?そんなことできるの?」

「複雑なAPIの連携テストをシンプルに表現したい」

「チーム全員が読み書きできるテストシナリオが欲しい」

できます! runnのシナリオ記述を学べば、プログラミング知識がなくても複雑なE2Eテストが書けるようになります。

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

runnでは、テストシナリオをRunbookと呼びます。

まるで映画の台本のように、何をどの順番で実行するかを記述する、それが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

見てください! これだけ直感的に書けるんです。

Runbookの5つの要素をマスターしよう

Runbookは5つの主要セクションで構成されています。それぞれが重要な役割を持っています:

1. desc - シナリオの目的を宣言

何をテストするのか、一目で分かるように記述します。チームメンバーへの最高のドキュメントです。

2. labels - スマートな分類

--labelオプションで特定のテストだけを実行。大規模プロジェクトの必須テクニック

3. runners - 接続先の定義

HTTP、gRPC、データベース...すべての接続先をここで定義。マルチプロトコル対応の心臓部

4. vars - 変数で効率化

繰り返し使う値を変数化。DRY原則をYAMLでも実現

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}) - 複雑なデータ構造もOK

変数参照の魔法

定義した変数を自在に活用

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リクエストの全機能

これがrunnの真骨頂! すべての機能を詰め込んだ例:

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": [
      "Tue, 22 Jul 2025 01:12:08 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もDBも、同じtest構文でアサーション!統一感が素晴らしい。

現場で使える!実践シナリオ集

マスターピース1: 完璧な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: アンカー&エイリアス

DRYの極みを実現:

# 共通のヘッダーを定義
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

あなたは今、YAMLマスター!

おめでとうございます!

この章を読み終えたあなたは、もう立派なRunbook作成者です。

習得したスキル

Runbookの5つの要素を完全理解

リスト形式とマップ形式を使い分けられる

変数を活用してDRYなシナリオが書ける

実践的なシナリオが作成できる

YAMLの高度なテクニックを身につけた

次なる高みへ

でも、これはまだ序章です!

次章では、runnの最強の武器である式評価エンジンを学びます。前のステップの結果を自在に操り、条件分岐やフィルタリングを駆使する...

もっとパワフルなテストが書けるようになります!


ヒント: この章で学んだ技術を組み合わせれば、どんな複雑なシナリオも表現できます。まずは小さなシナリオから始めて、徐々に大きくしていきましょう!