第6章:高度な機能編 - runnの真の力を解放せよ!

ここからが本番だ! 今まで学んできた機能はほんの序の口に過ぎない。この章では、runnの本当にすごい機能を伝授しよう!これらの機能をマスターすれば、どんなに複雑なテストシナリオも余裕だ!

🔁 ループ処理 - 繰り返しの魔法!

🎯 基本的なループ - 単純な繰り返しを極めろ!

desc: 基本的なループ処理
runners:
  api: https://api.example.com

vars:
  test_users:
    - name: "Alice"
      email: "alice@example.com"
    - name: "Bob"
      email: "bob@example.com"
    - name: "Charlie"
      email: "charlie@example.com"

steps:
  # 単純な回数指定ループ
  simple_loop:
    loop: 5
    req:
      api:///api/ping:
        get:
    test: current.res.status == 200
    dump:
      iteration: i  # ループインデックス(0から開始)

  # 配列の各要素に対するループ
  array_loop:
    loop:
      count: len(vars.test_users)
    req:
      api:///users:
        post:
          body:
            application/json:
              name: "{{ vars.test_users[i].name }}"
              email: "{{ vars.test_users[i].email }}"
    test: current.res.status == 201

🔄 条件付きループ(リトライ機能) - 諸めないテストの極意!

desc: 条件付きループ(リトライ機能)
runners:
  api: https://api.example.com

vars:
  operation_id: "op123"

steps:
  # 成功するまでリトライ
  retry_until_success:
    loop:
      count: 10  # 最大10回
      until: current.res.status == 200  # 成功条件
      minInterval: 1  # 最小間隔(秒)
      maxInterval: 5  # 最大間隔(秒)
    req:
      api:///unstable-endpoint:
        get:
    test: current.res.status == 200

  # 複雑な条件でのリトライ
  complex_retry:
    loop:
      count: 5
      until: |
        current.res.status == 200 &&
        current.res.body.status == "ready" &&
        len(current.res.body.items) > 0
      minInterval: 2
      maxInterval: 10
    req:
      api:///async-operation/{{ vars.operation_id }}/status:
        get:
    test: |
      current.res.body.status == "ready"

  # エラー条件でのループ終了
  stop_on_error:
    loop:
      count: 100
      until: current.res.status >= 400  # エラーが発生したら停止
    req:
      api:///batch-process:
        post:
          body:
            application/json:
              batch_id: "{{ i }}"
    test: |
      current.res.status < 400 ||  # 成功
      (current.res.status >= 400 && i > 0)  # エラーだが少なくとも1回は成功

🎮 動的なループ制御 - ループを思いのままに操れ!

desc: 動的なループ制御
runners:
  api: https://api.example.com

vars:
  page_size: 10
  max_pages: 100
  items_to_process:
    - id: 1
      name: "Item 1"
      needs_processing: true
    - id: 2
      name: "Item 2"
      needs_processing: false
    - id: 3
      name: "Item 3"
      needs_processing: true

steps:
  # ページネーションを使った全データ取得
  paginated_fetch:
    loop:
      count: vars.max_pages
      until: len(current.res.body.data) < vars.page_size  # 最後のページに到達
    req:
      api:///users:
        get:
          query:
            page: "{{ i + 1 }}"
            limit: "{{ vars.page_size }}"
    test: current.res.status == 200
    dump:
      page_number: i + 1
      items_count: len(current.res.body.data)
      total_fetched: |
        sum(map(steps.paginated_fetch, {len(.res.body.data)}))

  # 条件に基づく動的ループ
  conditional_processing:
    loop:
      count: len(vars.items_to_process)
    if: vars.items_to_process[i].needs_processing
    req:
      api:///process:
        post:
          body:
            application/json: "{{ vars.items_to_process[i] }}"
    test: current.res.status == 200

🔀 条件付き実行 - 賢いテストの秘訣!

🎆 基本的な条件分岐 - if文でテストを制御!

desc: 基本的な条件分岐
runners:
  api: https://api.example.com

vars:
  enable_new_feature: true

steps:
  # 環境による条件分岐
  environment_specific:
    if: env.ENVIRONMENT == "production"
    req:
      api:///production-only-endpoint:
        get:
    test: current.res.status == 200

  # 変数による条件分岐
  feature_flag_check:
    if: vars.enable_new_feature
    req:
      api:///new-feature:
        get:
    test: current.res.status == 200

  # ユーザー作成ステップ(常に実行)
  user_creation:
    req:
      api:///users:
        post:
          body:
            application/json:
              name: "Test User"
              email: "test@example.com"
    test: |
      current.res.status == 201 || current.res.status == 409

  # 前のステップの結果による条件分岐
  conditional_on_previous:
    if: steps.user_creation.res.status == 201
    req:
      api:///users/{{ steps.user_creation.res.body.id }}/activate:
        post:
    test: current.res.status == 200

🧠 複雑な条件式 - どんな条件も表現できる!

desc: 複雑な条件式
runners:
  api: https://api.example.com

vars:
  user_role: "admin"
  user_permissions:
    - read
    - write
    - delete
  api_version: 2
  maintenance_end: "2024-01-01T00:00:00Z"
  test_data:
    - id: 1
      name: "Test Data 1"
  resource_data:
    name: "Protected Resource"
    type: "document"

steps:
  complex_conditions:
    if: |
      (env.ENVIRONMENT == "test" || env.ENVIRONMENT == "staging") &&
      vars.user_role == "admin" &&
      len(vars.test_data) > 0
    req:
      api:///admin/test-data:
        post:
          body:
            application/json: "{{ vars.test_data }}"
    test: current.res.status == 201

  # 複数条件の組み合わせ
  multi_condition_check:
    if: |
      vars.api_version >= 2 &&
      (vars.user_permissions contains "write" || vars.user_role == "admin") &&
      time.now() > time.parse(vars.maintenance_end, time.RFC3339)
    req:
      api:///v2/protected-resource:
        post:
          body:
            application/json:
              action: "create"
              data: "{{ vars.resource_data }}"
    test: current.res.status in [200, 201]

🛡️ エラーハンドリングと条件分岐 - 失敗を成功に変えろ!

desc: エラーハンドリングと条件分岐
runners:
  api: https://api.example.com

steps:
  # エラー時の代替処理
  primary_request:
    req:
      api:///primary-endpoint:
        get:
    test: true  # エラーでも続行

  fallback_request:
    if: steps.primary_request.res.status != 200
    req:
      api:///fallback-endpoint:
        get:
    test: current.res.status == 200

  # 成功時の追加処理
  success_processing:
    if: |
      steps.primary_request.res.status == 200 ||
      steps.fallback_request.res.status == 200
    req:
      api:///process-result:
        post:
          body:
            application/json:
              source: |
                steps.primary_request.res.status == 200 ? "primary" : "fallback"
              data: |
                steps.primary_request.res.status == 200 ? 
                steps.primary_request.res.body : 
                steps.fallback_request.res.body
    test: current.res.status == 200

📦 シナリオのインクルード - DRYの極意!

🔗 基本的なインクルード - シナリオを再利用せよ!

# main.yml
desc: メインシナリオ
vars:
  base_url: https://api.example.com
  user_id: 123
  test_username: "testuser"
  test_password: "testpass123"

steps:
  # 共通の認証処理をインクルード
  - include:
      path: ./common/auth.yml
      vars:
        username: "{{ vars.test_username }}"
        password: "{{ vars.test_password }}"

  # ユーザー操作をインクルード
  - include:
      path: ./user/user_operations.yml
      vars:
        user_id: "{{ vars.user_id }}"
        auth_token: "{{ steps[0].auth_token }}"

  # クリーンアップ処理をインクルード
  - include:
      path: ./common/cleanup.yml
# common/auth.yml
desc: 認証処理
steps:
  login:
    req:
      "{{ parent.vars.base_url }}/auth/login":
        post:
          body:
            application/json:
              username: "{{ vars.username }}"
              password: "{{ vars.password }}"
    test: current.res.status == 200
    dump:
      auth_token: current.res.body.token

🎭 動的なインクルード - 実行時にシナリオを選択!

desc: 動的なインクルード
vars:
  test_type: "integration"
  integration_data:
    db_name: "test_db"
    test_user: "test_user"
  test_scenarios:
    - path: "./scenarios/user_test.yml"
      data:
        username: "alice"
    - path: "./scenarios/product_test.yml"
      data:
        product_id: 123
    - path: "./scenarios/order_test.yml"
      data:
        order_id: 456

steps:
  # 条件に基づくインクルード
  conditional_include:
    if: vars.test_type == "integration"
    include:
      path: ./integration/full_test.yml
      vars:
        test_data: "{{ vars.integration_data }}"

  # ループ内でのインクルード
  multiple_scenarios:
    loop:
      count: len(vars.test_scenarios)
    include:
      path: "{{ vars.test_scenarios[i].path }}"
      vars:
        scenario_data: "{{ vars.test_scenarios[i].data }}"
        iteration: "{{ i }}"

🏢 ネストしたインクルード - 階層的なシナリオ構築!

# level1.yml
desc: レベル1のシナリオ
steps:
  setup:
    include:
      path: ./setup/database.yml
      vars:
        db_name: "test_{{ time.unix(time.now()) }}"

  main_test:
    include:
      path: ./level2.yml
      vars:
        db_name: "{{ steps.setup.db_name }}"
# level2.yml
desc: レベル2のシナリオ
steps:
  data_preparation:
    include:
      path: ./data/prepare.yml
      vars:
        target_db: "{{ parent.vars.db_name }}"

  execute_test:
    include:
      path: ./level3.yml
      vars:
        prepared_data: "{{ steps.data_preparation.result }}"

⚡ 並行実行制御 - スピードの限界を突破!

🚀 基本的な並行実行 - 複数テストを同時実行!

desc: 並行実行の制御
# 同時実行数の制限
concurrency: 5

runners:
  api: https://api.example.com

vars:
  api_endpoints:
    - "/api/health"
    - "/api/status"
    - "/api/version"
    - "/api/ping"
    - "/api/ready"
    - "/api/metrics"
    - "/api/info"
    - "/api/check"

steps:
  # 複数のAPIエンドポイントを並行テスト
  parallel_api_tests:
    loop:
      count: len(vars.api_endpoints)
    req:
      "{{ vars.api_endpoints[i] }}":
        get:
    test: current.res.status == 200

🔒 共有リソースの制御 - 競合を防ぐ鉄壁の守り!

desc: 共有リソースの制御
# データベースを使用するテストは同時に1つだけ実行
concurrency: use-database

runners:
  db: postgres://user:pass@localhost:5432/testdb?sslmode=disable

steps:
  database_test:
    db:
      db:///:
        query: |
          INSERT INTO test_table (data) VALUES ($1)
        params:
          - "{{ faker.randomString(10) }}"
    test: current.rowsAffected == 1

🎆 複雑な並行制御 - プロフェッショナルの技!

desc: 複雑な並行実行制御
vars:
  worker_count: 10
  batch_size: 100

steps:
  # ワーカープロセスのシミュレーション
  worker_simulation:
    loop:
      count: vars.worker_count
    include:
      path: ./worker/process_batch.yml
      vars:
        worker_id: "{{ i }}"
        batch_start: "{{ i * vars.batch_size }}"
        batch_end: "{{ (i + 1) * vars.batch_size }}"

  # 結果の集約
  aggregate_results:
    dump:
      total_processed: |
        sum(map(steps.worker_simulation, {.processed_count}))
      success_rate: |
        sum(map(steps.worker_simulation, {.success_count})) / 
        sum(map(steps.worker_simulation, {.processed_count}))

🌐 依存関係の定義 - テストの流れを完全制御!

🔗 基本的な依存関係 - 順番を守る賢いテスト!

desc: 依存関係のあるテスト
needs:
  setup: ./setup/environment.yml
  data: ./setup/test_data.yml

runners:
  api: https://api.example.com

steps:
  main_test:
    req:
      api:///api/test:
        get:
          headers:
            Authorization: "Bearer {{ needs.setup.auth_token }}"
    test: current.res.status == 200

  data_validation:
    test: |
      current.res.body.count == needs.data.expected_count

🕸️ 複雑な依存関係 - ネットワーク状の依存も余裕!

desc: 複雑な依存関係の管理
needs:
  # 基本セットアップ
  base_setup: ./setup/base.yml

  # データベースセットアップ(base_setupに依存)
  db_setup:
    path: ./setup/database.yml
    needs:
      - base_setup

  # ユーザーデータ準備(db_setupに依存)
  user_data:
    path: ./data/users.yml
    needs:
      - db_setup

  # 商品データ準備(db_setupに依存)
  product_data:
    path: ./data/products.yml
    needs:
      - db_setup

runners:
  api: https://api.example.com

steps:
  integration_test:
    req:
      api:///api/orders:
        post:
          body:
            application/json:
              user_id: "{{ needs.user_data.test_user_id }}"
              product_id: "{{ needs.product_data.test_product_id }}"
              quantity: 1
    test: current.res.status == 201

🔧 カスタムランナーの作成 - 独自のランナーを生み出せ!

🔌 プラグインランナーの定義 - runnを拡張せよ!

desc: カスタムランナーの使用例
runners:
  # カスタムHTTPクライアント
  custom_http:
    type: http
    base_url: https://api.example.com
    default_headers:
      User-Agent: "MyApp/1.0"
      Accept: "application/json"
    timeout: 30s
    retry_count: 3

  # カスタムデータベース接続
  custom_db:
    type: db
    dsn: "{{ env.DATABASE_URL }}"
    max_connections: 10
    connection_timeout: 5s

steps:
  custom_request:
    req:
      custom_http:///users:
        get:
          headers:
            X-Custom-Header: "custom-value"
    test: current.res.status == 200

🌍 外部コマンドランナー - どんなツールも統合!

desc: 外部コマンドランナー
vars:
  namespace: "default"
  project_root: "/app"
  image_name: "myapp"
  tag: "latest"

runners:
  # 外部ツールの実行
  kubectl:
    type: exec
    command: kubectl
    default_args:
      - --kubeconfig={{ env.KUBECONFIG }}
      - --namespace={{ vars.namespace }}

  # Docker操作
  docker:
    type: exec
    command: docker
    working_dir: "{{ vars.project_root }}"

steps:
  k8s_deployment:
    exec:
      kubectl:///:
        args:
          - apply
          - -f
          - deployment.yaml
    test: current.exit_code == 0

  docker_build:
    exec:
      docker:///:
        args:
          - build
          - -t
          - "{{ vars.image_name }}:{{ vars.tag }}"
          - .
    test: current.exit_code == 0

🎭 高度なデータ処理 - データを魔法のように操れ!

🎲 動的なテストデータ生成 - 無限のテストデータ!

desc: 動的なテストデータ生成
runners:
  api: https://api.example.com

vars:
  # 動的なテストデータ生成
  test_users: |
    map(range(1, 11), {
      "id": .,
      "name": faker.name(),
      "email": faker.email(),
      "age": faker.randomInt(18, 65),
      "department": faker.randomChoice(["IT", "Sales", "Marketing", "HR"]),
      "salary": faker.randomInt(30000, 100000),
      "active": faker.randomBool()
    })

steps:
  bulk_user_creation:
    loop:
      count: len(vars.test_users)
    req:
      api:///users:
        post:
          body:
            application/json: "{{ vars.test_users[i] }}"
    test: current.res.status == 201

  # 生成されたデータの統計分析
  data_analysis:
    dump:
      total_users: len(vars.test_users)
      active_users: len(filter(vars.test_users, {.active}))
      avg_age: |
        sum(map(vars.test_users, {.age})) / len(vars.test_users)
      departments: |
        groupBy(vars.test_users, {.department})
      salary_stats:
        min: min(map(vars.test_users, {.salary}))
        max: max(map(vars.test_users, {.salary}))
        avg: |
          sum(map(vars.test_users, {.salary})) / len(vars.test_users)

🌀 複雑なデータ変換 - データの形を自在に変えろ!

desc: 複雑なデータ変換
runners:
  api: https://api.example.com

steps:
  data_transformation:
    req:
      api:///api/raw-data:
        get:

    dump:
      # 生データの変換
      transformed_data: |
        map(current.res.body.items, {
          merge(
            pick(., ["id", "name", "created_at"]),
            {
              "display_name": upper(.name),
              "age_days": time.sub(time.now(), time.parse(.created_at, time.RFC3339)) / time.day,
              "category": .price > 1000 ? "premium" : "standard",
              "tags": split(.tag_string, ","),
              "metadata": fromJSON(.metadata_json ?? "{}")
            }
          )
        })

      # 集約データの生成
      summary: |
        let items = current.res.body.items;
        {
          "total_count": len(items),
          "premium_count": len(filter(items, {.price > 1000})),
          "categories": unique(map(items, {.category})),
          "price_ranges": groupBy(items, {
            .price < 100 ? "low" :
            .price < 1000 ? "medium" : "high"
          }),
          "recent_items": filter(items, {
            time.sub(time.now(), time.parse(.created_at, time.RFC3339)) < time.day * 7
          })
        }

🛡️ エラーハンドリングとデバッグ - トラブルシューティングの達人!

💜 包括的なエラーハンドリング - どんなエラーも恐れない!

desc: 包括的なエラーハンドリング
runners:
  api: https://api.example.com

vars:
  resource_id: "res123"

steps:
  robust_api_call:
    loop:
      count: 3
      until: |
        current.res.status == 200 ||
        current.res.status == 404  # 404は正常な結果として扱う
      minInterval: 1
      maxInterval: 5
    req:
      api:///api/resource/{{ vars.resource_id }}:
        get:
    test: |
      current.res.status in [200, 404]

    dump:
      # エラー情報の詳細記録
      error_info: |
        current.res.status >= 400 ? {
          "status": current.res.status,
          "error": current.res.body.error ?? "Unknown error",
          "timestamp": time.now(),
          "attempt": i + 1
        } : null

🔍 デバッグ情報の出力 - 問題を瞬時に特定!

desc: デバッグ情報の出力
runners:
  api: https://api.example.com

vars:
  complex_data:
    operation: "create"
    resource: "user"
    data:
      name: "Test User"
      email: "test@example.com"

steps:
  debug_step:
    req:
      api:///api/complex-operation:
        post:
          body:
            application/json: "{{ vars.complex_data }}"

    # 詳細なデバッグ情報
    dump:
      request_info:
        url: current.req.url
        method: current.req.method
        headers: current.req.headers
        body_size: len(toJSON(current.req.body))

      response_info:
        status: current.res.status
        headers: current.res.headers
        body_size: len(toJSON(current.res.body))
        response_time: current.res.response_time

      validation_details:
        expected_fields: ["id", "name", "status"]
        actual_fields: keys(current.res.body)
        missing_fields: |
          filter(["id", "name", "status"], {
            !(. in keys(current.res.body))
          })
        extra_fields: |
          filter(keys(current.res.body), {
            !(. in ["id", "name", "status"])
          })

🎆 まとめ - 高度な機能のマスター誕生!

やったぞ! あなたは今、runnの真の力を解放した!

🏆 この章で手に入れた8つの超能力:

  1. 🔁 ループ処理: 繰り返しを完全に支配!リトライも思いのまま!
  2. 🔀 条件付き実行: 賢いテストが自分で判断
  3. 📦 シナリオのインクルード: DRY原則を極限まで追求
  4. ⚡ 並行実行制御: スピードの限界を突破
  5. 🌐 依存関係の定義: 複雑なフローも美しく制御
  6. 🔧 カスタムランナー: あなただけの特別なランナーを作れ!
  7. 🎭 高度なデータ処理: データを魔法のように変形
  8. 🛡️ エラーハンドリング: どんなエラーも恐れない

これらの機能を絶妙に組み合わせれば、あなたのテストは芸術作品に昇華する。もう、どんなに複雑なプロダクション環境も完璧にテストできる!

次章では、runnの最終兵器、Goテストヘルパーとしての使い方を伝授しよう! これであなたも真のrunnマスターだ!

第7章:Goテストヘルパー編へ →