第1章:基礎編

runnとは何か

「APIテストを書くのが面倒だ」「複雑なシナリオテストをもっとシンプルに書きたい」「テストコードのメンテナンスに疲れた」

そんなあなたに、runnという革命的なツールを紹介します。

runn(「Run N」と読み、/rʌ́n én/と発音)は、k1LoW氏によって開発された、シナリオベースのテスト・自動化ツールです。2022年の登場以来、そのシンプルさと強力さで多くの開発者を魅了し続けています。

なぜrunnが必要なのか?

従来のAPIテストツールやE2Eテストフレームワークには、こんな課題がありました:

  • 学習コストが高い: 独自のDSLや複雑なAPIを覚える必要がある
  • コードが冗長: シンプルなテストでも大量のボイラープレートコードが必要
  • メンテナンスが大変: テストコードの修正に時間がかかる
  • プロトコルごとに別ツール: HTTP、gRPC、DBテストで異なるツールを使い分ける必要がある

runnは、これらの課題をYAMLベースの宣言的な記述で解決します:

# これだけでAPIテストが完成!
desc: ユーザー登録から認証までの一連のフロー

runners:
  blog: http://localhost:8080

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

runnの3つの顔

runnは単なるAPIテストツールではありません。以下の3つの側面を持つ、マルチパーパスツールです:

1. 🎯 シナリオベースのテストツール

YAMLでテストシナリオを記述し、CLIから実行。学習コスト最小で始められます。

2. 🔧 Go言語のテストヘルパー

go testにシームレスに統合。既存のGoプロジェクトにそのまま導入できます。

3. 🤖 汎用的な自動化ツール

CI/CDでの活用、定期実行タスク、運用自動化など、テスト以外でも大活躍

runnの革新的な特徴

🌐 マルチプロトコル対応

HTTP、gRPC、データベース、ブラウザ操作、SSHまで、すべて同じYAML形式で記述できます。もう複数のツールを使い分ける必要はありません。

# HTTPもgRPCもDBも、すべて同じ形式!
steps:
  - req: { /users: { get: {} } }           # HTTP
  - grpc: { getUser: { id: 1 } }          # gRPC
  - db: { query: "SELECT * FROM users" }   # Database

📦 シングルバイナリ

依存関係なし、環境構築不要。ダウンロードしてすぐ使えるシンプルさ。CI環境でも curl 一発でインストール完了。

🔗 強力な式評価エンジン

前のステップの結果を次のステップで利用するステップ間連携が自由自在。複雑なシナリオも直感的に記述できます。

desc: ステップ間でデータを連携

runners:
  blog: http://localhost:8080

steps:
  login:
    blog:
      /auth:
        post:
          body:
            application/json:
              username: "alice"
              password: "secret"
  get_profile:
    blog:
      /profile:
        get:
          headers:
            # 前のステップで取得したトークンを使用
            Authorization: "Bearer {{ steps.login.res.body.token }}"

🚀 Goテスト統合

既存のGoプロジェクトに1ファイル追加するだけで導入可能。テストデータベースのセットアップや並列実行も自由自在。

誰のためのツール?

  • APIを開発している人: シンプルで保守しやすいE2Eテストを書きたい
  • QAエンジニア: プログラミング知識なしでもテストシナリオを作成したい
  • SRE/インフラエンジニア: 運用タスクを自動化したい
  • Goプログラマー: 既存のテストフレームワークと統合したい

なぜ今、runnなのか?

マイクロサービス化が進む現代、複数のサービス間の連携テストはますます重要になっています。しかし、既存のツールでは複雑なシナリオの記述が困難でした。

runnは、「シンプルに書けて、パワフルに実行できる」という理想を実現したツールです。YAMLという親しみやすい形式で、誰でも読み書きできるテストシナリオを作成できます。

さあ、次のセクションでrunnをインストールして、この革命的なツールの威力を体感してみましょう!

今すぐrunnを始めよう!

「よし、runnを使ってみたい!」

その意欲に応えるため、最速でrunnを始められる方法を紹介します。

爆速インストールガイド

Homebrew - macOS/Linuxユーザーの最速ルート

たった1行で完了!コーヒーを淹れる時間すら必要ありません:

brew install k1LoW/tap/runn

Go install - Gopherたちの選択

Go環境がある?それなら話は早い:

go install github.com/k1LoW/runn/cmd/runn@latest

Pro tip: go installなら最新の開発版も簡単に試せます!

直接ダウンロード - シンプル・イズ・ベスト

GitHub Releasesから、あなたの環境に合ったバイナリを選んでダウンロード。解凍して配置するだけで準備完了!

  • Windows? ✅ 対応
  • Mac (Intel/Apple Silicon)? ✅ 対応
  • Linux (各種アーキテクチャ)? ✅ 対応

🐳 Docker - 環境を汚したくない慎重派のあなたへ

ローカル環境は綺麗に保ちたい?Dockerで隔離実行:

docker container run -it --rm --name runn -v $PWD:/books ghcr.io/k1low/runn:latest list /books/*.yml

インストール成功の瞬間

ドキドキの確認タイム:

runn --version

バージョン番号が表示されたら、おめでとうございます!
あなたは今、runnの世界への扉を開きました。

runnマスターへの第一歩

インストール完了?素晴らしい!

では、runnの基本コマンドをマスターして、テスト自動化の達人への道を歩み始めましょう。

必須コマンド - これだけ覚えれば今すぐ使える!

runn run - シナリオを実行する魔法の呪文

一番使うコマンドがこれ!

# 単一シナリオの実行
runn run scenario.yml

# ワイルドカードで複数実行 - まとめてテスト!
runn run scenarios/**/*.yml

runn list - シナリオの棚卸し

どんなテストがあるか一目瞭然:

runn list scenarios/

runn new - シナリオ自動生成の魔術

これがrunnの隠れた最強機能! 既存のcurlコマンドやアクセスログから、自動でシナリオを生成:

# curlコマンドを即座にシナリオ化!
runn new --and-run --out first.yml -- curl https://httpbin.org/get

# アクセスログから一括生成 - 過去の通信を再現!
cat access.log | runn new --out generated.yml

驚きの事実: 手動でYAMLを書く前に、まずrunn newを試してみて!

パワーユーザー向けオプション

--verbose - 詳細ログで問題を即座に発見

デバッグの強い味方:

runn run scenario.yml --verbose

何が起きているか、すべてが見える!

--label - スマートなシナリオ管理

大規模プロジェクトでの必須テクニック:

# 重要なAPIテストだけを実行
runn run scenarios/**/*.yml --label api --label critical

--concurrent - 並列実行で爆速テスト

時間は金なり!並列度を上げて高速化:

runn run scenarios/**/*.yml --concurrent 5

注意: サーバーに優しく。並列度は適切に!

--fail-fast - 失敗したら即停止

CI/CDでの時間節約テクニック:

runn run scenarios/**/*.yml --fail-fast

最初のエラーで停止。無駄な待ち時間とはお別れ!

あなたの最初のrunnシナリオ!

理論は十分。実践の時間です!

5分後には、あなたも立派なrunnマスターになっているでしょう。

テスト環境をサクッと準備

まずはテスト用のAPIサーバーを立ち上げましょう。go-httpbinという便利なツールを使います:

docker run -p 8080:8080 mccutchen/go-httpbin

起動した? よし、これであなた専用のテスト環境の完成です!

Scenario 1: Hello, runn! - 記念すべき第一歩

examples/basics/first-scenario.ymlとして保存:

desc: HTTPBinにGETリクエストを送信

runners:
  httpbin: http://localhost:8080

steps:
  - httpbin:
      /get:
        get:
          headers:
            User-Agent: runn/1.0
    test: |
      current.res.status == 200

たったこれだけ! でも、これが立派なE2Eテストなんです。

実行してみよう!

ドキドキの瞬間:

runn run examples/basics/first-scenario.yml --verbose

実行結果

1 scenario, 0 skipped, 0 failures

見ました? okの文字が!これがあなたの初めてのテスト成功です!

Scenario 2: JSONレスポンスを賢く検証

APIテストの真骨頂、レスポンスの中身まで検証してみましょう:

desc: JSONレスポンスの内容を検証
runners:
  httpbin: http://localhost:8080

steps:
  - httpbin:
      /json:
        get: {}
    test: |
      current.res.status == 200 &&
      current.res.body.slideshow.title == "Sample Slide Show"

ポイント: current.res.bodyで自由自在にJSONの中身をチェック!

Scenario 3: 変数でDRYに

同じ値を何度も書くのは面倒?変数を使ってスマートに

desc: 変数を使用したPOSTリクエスト

runners:
  httpbin: http://localhost:8080

vars:
  username: testuser
  email: test@example.com

steps:
  - httpbin:
      /post:
        post:
          body:
            application/json:
              name: "{{ vars.username }}"
              email: "{{ vars.email }}"
    test: |
      current.res.status == 200 &&
      current.res.body.json.name == vars.username

魔法のような{{ vars }}記法で、メンテナンスが劇的に楽に!

Scenario 4: ステップ連携の威力

これぞrunnの真骨頂!前のステップの結果を次で使う

desc: ログインしてからデータを取得

runners:
  httpbin: http://localhost:8080

steps:
  # ステップ1: ログイン(シミュレーション)
  login:
    httpbin:
      /post:
        post:
          body:
            application/json:
              username: alice
              password: secret123
    test: current.res.status == 200

  # ステップ2: 認証が必要なエンドポイントにアクセス
  get_data:
    httpbin:
      /bearer:
        get:
          headers:
            # 前のステップの結果を使用(実際のAPIではトークンが返される想定)
            Authorization: "Bearer dummy-token-{{ steps.login.res.body.json.username }}"
    test: |
      current.res.status == 200

驚きポイント: steps.login.res.body.json.usernameで前のレスポンスを参照! 実際のログインフローをそのままテストできます。

runnの2つの顔 - あなたはどっち派?

CLIツール?Goテストヘルパー?

使い分けをマスターすれば、runnの真の力を引き出せます!

CLIツール派 - シンプル&クイック

こんな時はCLIで決まり!

手動でサクッとAPI確認

開発中の動作確認に最適。ターミナルから即実行!

CI/CDパイプラインで活躍

GitHub ActionsやGitLab CIに組み込んで自動テスト。設定も簡単!

外部APIの監視

定期的にヘルスチェック。cronと組み合わせて24時間監視体制!

開発中のデバッグ

--verboseオプションで詳細ログ。問題箇所を即座に特定!

Goテストヘルパー派 - パワフル&フレキシブル

プロの選択はこっち! 本格的なプロジェクトでの威力は絶大:

Goアプリケーションとの完璧な統合

go testコマンドでそのまま実行。既存のテストフローに自然に組み込める!

テストDBの自在な制御

db := setupTestDB(t)  // テスト用DBを準備
defer cleanupDB(db)   // 自動でクリーンアップ

モックサーバーとの連携

mockServer := httptest.NewServer(mockHandler)
// runnでモックサーバーに対してテスト実行

複雑なテストデータの準備

Goコードでデータを生成してから、runnでシナリオテスト。最強の組み合わせ!

実践例:Goテストヘルパーの威力を体感

これが現場で使われている本物のコードです:

package main

import (
    "context"
    "database/sql"
    "net/http/httptest"
    "testing"

    "github.com/k1LoW/runn"
)

func setupTestDB(t *testing.T) *sql.DB {
    db, err := setupDB()
    if err != nil {
        t.Fatal(err)
    }
    return db
}

func TestUserAPI(t *testing.T) {
    // テスト用サーバーを起動
    db := setupTestDB(t)
    defer func() {
        err := db.Close()
        if err != nil {
            t.Fatal(err)
        }
    }()

    srv := httptest.NewServer(NewApp(db))
    defer srv.Close()

    // runnでテストを実行
    opts := []runn.Option{
        runn.T(t),
        runn.Runner("blog", srv.URL),
        runn.Scopes("read:parent"),
    }

    o, err := runn.Load("../user-api-test.yml", opts...)
    if err != nil {
        t.Fatal(err)
    }

    if err := o.RunN(context.Background()); err != nil {
        t.Fatal(err)
    }
}

テスト対象サーバーの実装

まずは実際に動くAPIサーバーを見てみましょう:

package main

import (
    "database/sql"
    "encoding/json"
    "fmt"
    "log"
    "net/http"

    _ "github.com/mattn/go-sqlite3"
)

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

type App struct {
    db *sql.DB
}

func NewApp(db *sql.DB) http.Handler {
    app := &App{db: db}
    mux := http.NewServeMux()

    // ユーザー作成
    mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
        if r.Method == http.MethodPost {
            var user User
            if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
                http.Error(w, err.Error(), http.StatusBadRequest)
                return
            }

            result, err := app.db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", user.Name, user.Email)
            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
            }

            id, _ := result.LastInsertId()
            user.ID = int(id)

            w.Header().Set("Content-Type", "application/json")
            w.WriteHeader(http.StatusCreated)
            json.NewEncoder(w).Encode(user)
        }
    })

    // ユーザー取得
    mux.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
        if r.Method == http.MethodGet {
            id := r.URL.Path[len("/users/"):]

            var user User
            err := app.db.QueryRow("SELECT id, name, email FROM users WHERE id = ?", id).Scan(&user.ID, &user.Name, &user.Email)
            if err != nil {
                http.Error(w, "User not found", http.StatusNotFound)
                return
            }

            w.Header().Set("Content-Type", "application/json")
            json.NewEncoder(w).Encode(user)
        }
    })

    return mux
}

func setupDB() (*sql.DB, error) {
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        return nil, err
    }

    _, err = db.Exec(`
        CREATE TABLE users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            email TEXT NOT NULL
        )
    `)
    if err != nil {
        return nil, err
    }

    return db, nil
}

func main() {
    db, err := setupDB()
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    app := NewApp(db)

    fmt.Println("Server starting on :8081...")
    log.Fatal(http.ListenAndServe(":8081", app))
}

見どころ: SQLiteのインメモリDBを使った本格的なREST API!

魔法のYAMLシナリオ

そして、このサーバーをテストする美しいシナリオ

desc: ユーザーAPIのテスト

runners:
  blog: http://localhost:8080

steps:
  # ユーザーを作成
  create_user:
    blog:
      /users:
        post:
          body:
            application/json:
              name: "テスト太郎"
              email: "test@example.com"
    test: |
      current.res.status == 201 &&
      current.res.body.name == "テスト太郎" &&
      current.res.body.email == "test@example.com" &&
      current.res.body.id > 0

  # 作成したユーザーを取得
  get_user:
    blog:
      /users/{{ steps.create_user.res.body.id }}:
        get: {}
    test: |
      current.res.status == 200 &&
      current.res.body.id == steps.create_user.res.body.id &&
      current.res.body.name == "テスト太郎" &&
      current.res.body.email == "test@example.com"

  # 存在しないユーザーを取得(404エラー確認)
  get_nonexistent_user:
    blog:
      /users/9999:
        get: {}
    test: |
      current.res.status == 404

注目ポイント: - ユーザー作成 → 取得 → エラーケースまで完璧にカバー - steps.create_user.res.body.id動的にIDを参照 - たった36行で包括的なAPIテストが完成!

第1章完了!あなたは今...

おめでとうございます!

この章を読み終えたあなたは、もう立派なrunnユーザーです!

あなたが手に入れたスキル

runnの革命的な価値を理解した
最速でインストールできるようになった
基本コマンドをマスターした
実際にシナリオを書いて実行できた
CLIとGoヘルパーの使い分けを習得した

次のステップへの期待

第1章では基礎を固めました。でも、これはまだ序章に過ぎません!

次章では、より高度なシナリオ記述テクニックを学びます: - 条件分岐でスマートなテスト - ループ処理で効率的な検証 - 外部ファイル読み込みで大規模データ対応 - そしてもっと多くの魔法が...


ヒント: この章で学んだコマンドは、すぐに試してみることをお勧めします。実践こそが最高の学習方法です!