第5章:テストヘルパーとしての利用

runnはGoテストヘルパーとして使用でき、go testと統合してシナリオベースのテストを実行できます。

基本的な使い方

プロジェクト構造

myproject/
├── main.go
├── main_test.go
├── go.mod
└── testdata/
    └── api_test.yml

実装例

以下は、シンプルなユーザーAPI のテスト例です。

main.go:

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "sync"
)

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

type Server struct {
    mu      sync.RWMutex
    users   map[int]User
    nextID  int
}

func NewServer() *Server {
    return &Server{
        users:  make(map[int]User),
        nextID: 1,
    }
}

func (s *Server) Handler() http.Handler {
    mux := http.NewServeMux()
    mux.HandleFunc("/users", s.handleUsers)
    mux.HandleFunc("/users/", s.handleUser)
    mux.HandleFunc("/health", s.handleHealth)
    return mux
}

func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

func (s *Server) handleUsers(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        s.listUsers(w, r)
    case http.MethodPost:
        s.createUser(w, r)
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func (s *Server) handleUser(w http.ResponseWriter, r *http.Request) {
    var id int
    if _, err := fmt.Sscanf(r.URL.Path, "/users/%d", &id); err != nil {
        http.Error(w, "Invalid user ID", http.StatusBadRequest)
        return
    }

    switch r.Method {
    case http.MethodGet:
        s.getUser(w, r, id)
    case http.MethodDelete:
        s.deleteUser(w, r, id)
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}

func (s *Server) listUsers(w http.ResponseWriter, r *http.Request) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    users := make([]User, 0, len(s.users))
    for _, user := range s.users {
        users = append(users, user)
    }

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

func (s *Server) createUser(w http.ResponseWriter, r *http.Request) {
    var user User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, "Invalid request body", http.StatusBadRequest)
        return
    }

    s.mu.Lock()
    user.ID = s.nextID
    s.nextID++
    s.users[user.ID] = user
    s.mu.Unlock()

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

func (s *Server) getUser(w http.ResponseWriter, r *http.Request, id int) {
    s.mu.RLock()
    user, exists := s.users[id]
    s.mu.RUnlock()

    if !exists {
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }

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

func (s *Server) deleteUser(w http.ResponseWriter, r *http.Request, id int) {
    s.mu.Lock()
    _, exists := s.users[id]
    if exists {
        delete(s.users, id)
    }
    s.mu.Unlock()

    if !exists {
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }

    w.WriteHeader(http.StatusNoContent)
}

func main() {
    server := NewServer()
    fmt.Println("Server starting on :8080")
    if err := http.ListenAndServe(":8080", server.Handler()); err != nil {
        panic(err)
    }
}

main_test.go:

package main

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

    "github.com/k1LoW/runn"
)

func TestAPI(t *testing.T) {
    // テストサーバーの起動
    server := NewServer()
    ts := httptest.NewServer(server.Handler())
    defer ts.Close()

    // runnの設定
    opts := []runn.Option{
        runn.T(t),
        runn.Runner("api", ts.URL),
    }

    // YAMLシナリオの実行
    o, err := runn.Load("testdata/api_test.yml", opts...)
    if err != nil {
        t.Fatal(err)
    }

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

testdata/api_test.yml:

desc: ユーザーAPIのテスト
steps:
  # ヘルスチェック
  - desc: ヘルスチェックエンドポイントの確認
    api:
      /health:
        get: {}
    test: |
      current.res.status == 200 &&
      current.res.body.status == "ok"

  # ユーザー一覧(初期状態)
  - desc: 初期状態のユーザー一覧を確認
    api:
      /users:
        get: {}
    test: |
      current.res.status == 200 &&
      len(current.res.body) == 0

  # ユーザー作成
  - desc: 新規ユーザーを作成
    api:
      /users:
        post:
          body:
            application/json:
              name: "Alice"
              email: "alice@example.com"
    test: |
      current.res.status == 201 &&
      current.res.body.id == 1 &&
      current.res.body.name == "Alice" &&
      current.res.body.email == "alice@example.com"

  # 作成したユーザーの取得
  - desc: 作成したユーザーを取得
    api:
      /users/1:
        get: {}
    test: |
      current.res.status == 200 &&
      current.res.body.id == 1 &&
      current.res.body.name == "Alice"

  # ユーザー一覧(作成後)
  - desc: ユーザー作成後の一覧を確認
    api:
      /users:
        get: {}
    test: |
      current.res.status == 200 &&
      len(current.res.body) == 1 &&
      current.res.body[0].name == "Alice"

  # 存在しないユーザーの取得
  - desc: 存在しないユーザーを取得(エラーケース)
    api:
      /users/999:
        get: {}
    test: current.res.status == 404

  # ユーザー削除
  - desc: ユーザーを削除
    api:
      /users/1:
        delete: {}
    test: current.res.status == 204

  # 削除後の確認
  - desc: 削除したユーザーが存在しないことを確認
    api:
      /users/1:
        get: {}
    test: current.res.status == 404

実行方法

go test -v

主なオプション

  • runn.T(t) - testing.Tを渡してテスト統合
  • runn.Runner("name", url) - HTTPランナーの設定
  • runn.DBRunner("name", db) - データベースランナーの設定
  • runn.Var("key", value) - 変数の設定
  • runn.Debug(true) - デバッグモードの有効化

CI/CDでの実行

GitHub Actionsでの設定例:

.github/workflows/test.yml:

name: Test

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - name: Set up Go
      uses: actions/setup-go@v4
      with:
        go-version: '1.21'

    - name: Get dependencies
      run: go mod download

    - name: Run tests
      run: go test -v ./...

この設定により、プッシュのたびに自動的にrunnを使用したテストが実行されます。