第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を使用したテストが実行されます。