第1章:基礎編¶
runnとは何か¶
runn(「Run N」/rʌ́n én/)は、k1LoW氏が開発したシナリオベースのテスト・自動化ツールです。
特徴¶
マルチプロトコル対応¶
HTTP、gRPC、データベース、CDP(Chrome DevTools Protocol)、SSHを同一のYAML形式で記述できます。
# HTTPもgRPCもDBも、すべて同じ形式!
steps:
- req: { /users: { get: {} } } # HTTP
- grpc: { getUser: { id: 1 } } # gRPC
- db: { query: "SELECT * FROM users" } # Database
シングルバイナリ¶
単体で実行可能。ダウンロードしてすぐ使えます。
強力な式評価エンジン¶
前のステップの結果を次のステップで利用できます。
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 test
にシームレスに統合可能です。
用途¶
- APIのE2Eテスト
- CI/CDでの自動テスト
- 運用タスクの自動化
- APIの動作確認
インストール¶
Homebrew¶
brew install k1LoW/tap/runn
Go install¶
go install github.com/k1LoW/runn/cmd/runn@latest
直接ダウンロード¶
GitHub Releasesから環境に合ったバイナリをダウンロード。
Docker¶
docker container run -it --rm --name runn -v $PWD:/books ghcr.io/k1low/runn:latest list /books/*.yml
確認¶
runn --version
基本的な使い方¶
シナリオ実行¶
runn run scenario.yml
複数ファイルの実行:
runn run scenarios/**/*.yml
はじめてのシナリオ作成¶
テスト環境準備¶
docker run -p 8080:8080 mccutchen/go-httpbin
基本的なGETリクエスト¶
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
実行:
runn run examples/basics/first-scenario.yml --verbose
結果:
1 scenario, 0 skipped, 0 failures
JSONレスポンスの検証¶
desc: JSONレスポンスの内容を検証
runners:
httpbin: http://localhost:8080
steps:
- httpbin:
/json:
get: {}
test: |
current.res.status == 200 &&
current.res.body.slideshow.title == "Sample Slide Show"
変数の使用¶
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
ステップ間の連携¶
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
で前のステップの結果を参照できます。
CLIツール vs Goテストヘルパー¶
CLIツールとして¶
適した用途: - 手動でのAPI動作確認 - CI/CDパイプラインでの自動テスト - 外部APIの監視 - デバッグ作業
Goテストヘルパーとして¶
適した用途: - Goアプリケーションのテスト統合 - テストDBのセットアップ/クリーンアップ - モックサーバーとの連携 - 複雑なテストデータの準備
Goテストヘルパーの実装例¶
main_test.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サーバー(main.go
):
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))
}
テストシナリオ(user-api-test.yml
):
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
このように、SQLiteを使った本格的なREST APIのテストが、わずかなコードで実現できます。