mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-07 09:43:30 +08:00
960 lines
21 KiB
Markdown
960 lines
21 KiB
Markdown
---
|
||
name: golang-testing
|
||
description: テスト駆動開発とGoコードの高品質を保証するための包括的なテスト戦略。
|
||
---
|
||
|
||
# Go テスト
|
||
|
||
テスト駆動開発(TDD)とGoコードの高品質を保証するための包括的なテスト戦略。
|
||
|
||
## いつ有効化するか
|
||
|
||
- 新しいGoコードを書くとき
|
||
- Goコードをレビューするとき
|
||
- 既存のテストを改善するとき
|
||
- テストカバレッジを向上させるとき
|
||
- デバッグとバグ修正時
|
||
|
||
## 核となる原則
|
||
|
||
### 1. テスト駆動開発(TDD)ワークフロー
|
||
|
||
失敗するテストを書き、実装し、リファクタリングするサイクルに従います。
|
||
|
||
```go
|
||
// 1. テストを書く(失敗)
|
||
func TestCalculateTotal(t *testing.T) {
|
||
total := CalculateTotal([]float64{10.0, 20.0, 30.0})
|
||
want := 60.0
|
||
if total != want {
|
||
t.Errorf("got %f, want %f", total, want)
|
||
}
|
||
}
|
||
|
||
// 2. 実装する(テストを通す)
|
||
func CalculateTotal(prices []float64) float64 {
|
||
var total float64
|
||
for _, price := range prices {
|
||
total += price
|
||
}
|
||
return total
|
||
}
|
||
|
||
// 3. リファクタリング
|
||
// テストを壊さずにコードを改善
|
||
```
|
||
|
||
### 2. テーブル駆動テスト
|
||
|
||
複数のケースを体系的にテストします。
|
||
|
||
```go
|
||
func TestAdd(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
a, b int
|
||
want int
|
||
}{
|
||
{"positive numbers", 2, 3, 5},
|
||
{"negative numbers", -2, -3, -5},
|
||
{"mixed signs", -2, 3, 1},
|
||
{"zeros", 0, 0, 0},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
got := Add(tt.a, tt.b)
|
||
if got != tt.want {
|
||
t.Errorf("Add(%d, %d) = %d; want %d",
|
||
tt.a, tt.b, got, tt.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. サブテスト
|
||
|
||
サブテストを使用した論理的なテストの構成。
|
||
|
||
```go
|
||
func TestUser(t *testing.T) {
|
||
t.Run("validation", func(t *testing.T) {
|
||
t.Run("empty email", func(t *testing.T) {
|
||
user := User{Email: ""}
|
||
if err := user.Validate(); err == nil {
|
||
t.Error("expected validation error")
|
||
}
|
||
})
|
||
|
||
t.Run("valid email", func(t *testing.T) {
|
||
user := User{Email: "test@example.com"}
|
||
if err := user.Validate(); err != nil {
|
||
t.Errorf("unexpected error: %v", err)
|
||
}
|
||
})
|
||
})
|
||
|
||
t.Run("serialization", func(t *testing.T) {
|
||
// 別のテストグループ
|
||
})
|
||
}
|
||
```
|
||
|
||
## テスト構成
|
||
|
||
### ファイル構成
|
||
|
||
```text
|
||
mypackage/
|
||
├── user.go
|
||
├── user_test.go # ユニットテスト
|
||
├── integration_test.go # 統合テスト
|
||
├── testdata/ # テストフィクスチャ
|
||
│ ├── valid_user.json
|
||
│ └── invalid_user.json
|
||
└── export_test.go # 内部のテストのための非公開のエクスポート
|
||
```
|
||
|
||
### テストパッケージ
|
||
|
||
```go
|
||
// user_test.go - 同じパッケージ(ホワイトボックステスト)
|
||
package user
|
||
|
||
func TestInternalFunction(t *testing.T) {
|
||
// 内部をテストできる
|
||
}
|
||
|
||
// user_external_test.go - 外部パッケージ(ブラックボックステスト)
|
||
package user_test
|
||
|
||
import "myapp/user"
|
||
|
||
func TestPublicAPI(t *testing.T) {
|
||
// 公開APIのみをテスト
|
||
}
|
||
```
|
||
|
||
## アサーションとヘルパー
|
||
|
||
### 基本的なアサーション
|
||
|
||
```go
|
||
func TestBasicAssertions(t *testing.T) {
|
||
// 等価性
|
||
got := Calculate()
|
||
want := 42
|
||
if got != want {
|
||
t.Errorf("got %d, want %d", got, want)
|
||
}
|
||
|
||
// エラーチェック
|
||
_, err := Process()
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
|
||
// nil チェック
|
||
result := GetResult()
|
||
if result == nil {
|
||
t.Fatal("expected non-nil result")
|
||
}
|
||
}
|
||
```
|
||
|
||
### カスタムヘルパー関数
|
||
|
||
```go
|
||
// ヘルパーとしてマーク(スタックトレースに表示されない)
|
||
func assertEqual(t *testing.T, got, want interface{}) {
|
||
t.Helper()
|
||
if got != want {
|
||
t.Errorf("got %v, want %v", got, want)
|
||
}
|
||
}
|
||
|
||
func assertNoError(t *testing.T, err error) {
|
||
t.Helper()
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
}
|
||
|
||
// 使用例
|
||
func TestWithHelpers(t *testing.T) {
|
||
result, err := Process()
|
||
assertNoError(t, err)
|
||
assertEqual(t, result.Status, "success")
|
||
}
|
||
```
|
||
|
||
### ディープ等価性チェック
|
||
|
||
```go
|
||
import "reflect"
|
||
|
||
func assertDeepEqual(t *testing.T, got, want interface{}) {
|
||
t.Helper()
|
||
if !reflect.DeepEqual(got, want) {
|
||
t.Errorf("got %+v, want %+v", got, want)
|
||
}
|
||
}
|
||
|
||
func TestStructEquality(t *testing.T) {
|
||
got := User{Name: "Alice", Age: 30}
|
||
want := User{Name: "Alice", Age: 30}
|
||
assertDeepEqual(t, got, want)
|
||
}
|
||
```
|
||
|
||
## モッキングとスタブ
|
||
|
||
### インターフェースベースのモック
|
||
|
||
```go
|
||
// 本番コード
|
||
type UserStore interface {
|
||
GetUser(id string) (*User, error)
|
||
SaveUser(user *User) error
|
||
}
|
||
|
||
type UserService struct {
|
||
store UserStore
|
||
}
|
||
|
||
// テストコード
|
||
type MockUserStore struct {
|
||
users map[string]*User
|
||
err error
|
||
}
|
||
|
||
func (m *MockUserStore) GetUser(id string) (*User, error) {
|
||
if m.err != nil {
|
||
return nil, m.err
|
||
}
|
||
return m.users[id], nil
|
||
}
|
||
|
||
func (m *MockUserStore) SaveUser(user *User) error {
|
||
if m.err != nil {
|
||
return m.err
|
||
}
|
||
m.users[user.ID] = user
|
||
return nil
|
||
}
|
||
|
||
// テスト
|
||
func TestUserService(t *testing.T) {
|
||
mock := &MockUserStore{
|
||
users: make(map[string]*User),
|
||
}
|
||
service := &UserService{store: mock}
|
||
|
||
// サービスをテスト...
|
||
}
|
||
```
|
||
|
||
### 時間のモック
|
||
|
||
```go
|
||
// プロダクションコード - 時間を注入可能にする
|
||
type TimeProvider interface {
|
||
Now() time.Time
|
||
}
|
||
|
||
type RealTime struct{}
|
||
|
||
func (RealTime) Now() time.Time {
|
||
return time.Now()
|
||
}
|
||
|
||
type Service struct {
|
||
time TimeProvider
|
||
}
|
||
|
||
// テストコード
|
||
type MockTime struct {
|
||
current time.Time
|
||
}
|
||
|
||
func (m MockTime) Now() time.Time {
|
||
return m.current
|
||
}
|
||
|
||
func TestTimeDependent(t *testing.T) {
|
||
mockTime := MockTime{
|
||
current: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
||
}
|
||
service := &Service{time: mockTime}
|
||
|
||
// 固定時間でテスト...
|
||
}
|
||
```
|
||
|
||
### HTTP クライアントのモック
|
||
|
||
```go
|
||
type HTTPClient interface {
|
||
Do(req *http.Request) (*http.Response, error)
|
||
}
|
||
|
||
type MockHTTPClient struct {
|
||
response *http.Response
|
||
err error
|
||
}
|
||
|
||
func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
||
return m.response, m.err
|
||
}
|
||
|
||
func TestAPICall(t *testing.T) {
|
||
mockClient := &MockHTTPClient{
|
||
response: &http.Response{
|
||
StatusCode: 200,
|
||
Body: io.NopCloser(strings.NewReader(`{"status":"ok"}`)),
|
||
},
|
||
}
|
||
|
||
api := &APIClient{client: mockClient}
|
||
// APIクライアントをテスト...
|
||
}
|
||
```
|
||
|
||
## HTTPハンドラーのテスト
|
||
|
||
### httptest の使用
|
||
|
||
```go
|
||
func TestHandler(t *testing.T) {
|
||
handler := http.HandlerFunc(MyHandler)
|
||
|
||
req := httptest.NewRequest("GET", "/users/123", nil)
|
||
rec := httptest.NewRecorder()
|
||
|
||
handler.ServeHTTP(rec, req)
|
||
|
||
// ステータスコードをチェック
|
||
if rec.Code != http.StatusOK {
|
||
t.Errorf("got status %d, want %d", rec.Code, http.StatusOK)
|
||
}
|
||
|
||
// レスポンスボディをチェック
|
||
var response map[string]interface{}
|
||
if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
|
||
t.Fatalf("failed to decode response: %v", err)
|
||
}
|
||
|
||
if response["id"] != "123" {
|
||
t.Errorf("got id %v, want 123", response["id"])
|
||
}
|
||
}
|
||
```
|
||
|
||
### ミドルウェアのテスト
|
||
|
||
```go
|
||
func TestAuthMiddleware(t *testing.T) {
|
||
// ダミーハンドラー
|
||
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
w.WriteHeader(http.StatusOK)
|
||
})
|
||
|
||
// ミドルウェアでラップ
|
||
handler := AuthMiddleware(nextHandler)
|
||
|
||
tests := []struct {
|
||
name string
|
||
token string
|
||
wantStatus int
|
||
}{
|
||
{"valid token", "valid-token", http.StatusOK},
|
||
{"invalid token", "invalid", http.StatusUnauthorized},
|
||
{"no token", "", http.StatusUnauthorized},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
req := httptest.NewRequest("GET", "/", nil)
|
||
if tt.token != "" {
|
||
req.Header.Set("Authorization", "Bearer "+tt.token)
|
||
}
|
||
rec := httptest.NewRecorder()
|
||
|
||
handler.ServeHTTP(rec, req)
|
||
|
||
if rec.Code != tt.wantStatus {
|
||
t.Errorf("got status %d, want %d", rec.Code, tt.wantStatus)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
```
|
||
|
||
### テストサーバー
|
||
|
||
```go
|
||
func TestAPIIntegration(t *testing.T) {
|
||
// テストサーバーを作成
|
||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
json.NewEncoder(w).Encode(map[string]string{
|
||
"message": "hello",
|
||
})
|
||
}))
|
||
defer server.Close()
|
||
|
||
// 実際のHTTPリクエストを行う
|
||
resp, err := http.Get(server.URL)
|
||
if err != nil {
|
||
t.Fatalf("request failed: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// レスポンスを検証
|
||
var result map[string]string
|
||
json.NewDecoder(resp.Body).Decode(&result)
|
||
|
||
if result["message"] != "hello" {
|
||
t.Errorf("got %s, want hello", result["message"])
|
||
}
|
||
}
|
||
```
|
||
|
||
## データベーステスト
|
||
|
||
### トランザクションを使用したテストの分離
|
||
|
||
```go
|
||
func TestUserRepository(t *testing.T) {
|
||
db := setupTestDB(t)
|
||
defer db.Close()
|
||
|
||
tests := []struct {
|
||
name string
|
||
fn func(*testing.T, *sql.DB)
|
||
}{
|
||
{"create user", testCreateUser},
|
||
{"find user", testFindUser},
|
||
{"update user", testUpdateUser},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
tx, err := db.Begin()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer tx.Rollback() // テスト後にロールバック
|
||
|
||
tt.fn(t, tx)
|
||
})
|
||
}
|
||
}
|
||
```
|
||
|
||
### テストフィクスチャ
|
||
|
||
```go
|
||
func setupTestDB(t *testing.T) *sql.DB {
|
||
t.Helper()
|
||
|
||
db, err := sql.Open("postgres", "postgres://localhost/test")
|
||
if err != nil {
|
||
t.Fatalf("failed to connect: %v", err)
|
||
}
|
||
|
||
// スキーマを移行
|
||
if err := runMigrations(db); err != nil {
|
||
t.Fatalf("migrations failed: %v", err)
|
||
}
|
||
|
||
return db
|
||
}
|
||
|
||
func seedTestData(t *testing.T, db *sql.DB) {
|
||
t.Helper()
|
||
|
||
fixtures := []string{
|
||
`INSERT INTO users (id, email) VALUES ('1', 'test@example.com')`,
|
||
`INSERT INTO posts (id, user_id, title) VALUES ('1', '1', 'Test Post')`,
|
||
}
|
||
|
||
for _, query := range fixtures {
|
||
if _, err := db.Exec(query); err != nil {
|
||
t.Fatalf("failed to seed data: %v", err)
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## ベンチマーク
|
||
|
||
### 基本的なベンチマーク
|
||
|
||
```go
|
||
func BenchmarkCalculation(b *testing.B) {
|
||
for i := 0; i < b.N; i++ {
|
||
Calculate(100)
|
||
}
|
||
}
|
||
|
||
// メモリ割り当てを報告
|
||
func BenchmarkWithAllocs(b *testing.B) {
|
||
b.ReportAllocs()
|
||
for i := 0; i < b.N; i++ {
|
||
ProcessData([]byte("test data"))
|
||
}
|
||
}
|
||
```
|
||
|
||
### サブベンチマーク
|
||
|
||
```go
|
||
func BenchmarkEncoding(b *testing.B) {
|
||
data := generateTestData()
|
||
|
||
b.Run("json", func(b *testing.B) {
|
||
b.ReportAllocs()
|
||
for i := 0; i < b.N; i++ {
|
||
json.Marshal(data)
|
||
}
|
||
})
|
||
|
||
b.Run("gob", func(b *testing.B) {
|
||
b.ReportAllocs()
|
||
var buf bytes.Buffer
|
||
enc := gob.NewEncoder(&buf)
|
||
b.ResetTimer()
|
||
for i := 0; i < b.N; i++ {
|
||
enc.Encode(data)
|
||
buf.Reset()
|
||
}
|
||
})
|
||
}
|
||
```
|
||
|
||
### ベンチマーク比較
|
||
|
||
```go
|
||
// 実行: go test -bench=. -benchmem
|
||
func BenchmarkStringConcat(b *testing.B) {
|
||
b.Run("operator", func(b *testing.B) {
|
||
for i := 0; i < b.N; i++ {
|
||
_ = "hello" + " " + "world"
|
||
}
|
||
})
|
||
|
||
b.Run("fmt.Sprintf", func(b *testing.B) {
|
||
for i := 0; i < b.N; i++ {
|
||
_ = fmt.Sprintf("%s %s", "hello", "world")
|
||
}
|
||
})
|
||
|
||
b.Run("strings.Builder", func(b *testing.B) {
|
||
for i := 0; i < b.N; i++ {
|
||
var sb strings.Builder
|
||
sb.WriteString("hello")
|
||
sb.WriteString(" ")
|
||
sb.WriteString("world")
|
||
_ = sb.String()
|
||
}
|
||
})
|
||
}
|
||
```
|
||
|
||
## ファジングテスト
|
||
|
||
### 基本的なファズテスト(Go 1.18+)
|
||
|
||
```go
|
||
func FuzzParseInput(f *testing.F) {
|
||
// シードコーパス
|
||
f.Add("hello")
|
||
f.Add("world")
|
||
f.Add("123")
|
||
|
||
f.Fuzz(func(t *testing.T, input string) {
|
||
// パースがパニックしないことを確認
|
||
result, err := ParseInput(input)
|
||
|
||
// エラーがあっても、nilでないか一貫性があることを確認
|
||
if err == nil && result == nil {
|
||
t.Error("got nil result with no error")
|
||
}
|
||
})
|
||
}
|
||
```
|
||
|
||
### より複雑なファジング
|
||
|
||
```go
|
||
func FuzzJSONParsing(f *testing.F) {
|
||
f.Add([]byte(`{"name":"test","age":30}`))
|
||
f.Add([]byte(`{"name":"","age":0}`))
|
||
|
||
f.Fuzz(func(t *testing.T, data []byte) {
|
||
var user User
|
||
err := json.Unmarshal(data, &user)
|
||
|
||
// JSONがデコードされる場合、再度エンコードできるべき
|
||
if err == nil {
|
||
_, err := json.Marshal(user)
|
||
if err != nil {
|
||
t.Errorf("marshal failed after successful unmarshal: %v", err)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
```
|
||
|
||
## テストカバレッジ
|
||
|
||
### カバレッジの実行と表示
|
||
|
||
```bash
|
||
# カバレッジを実行してHTMLレポートを生成
|
||
go test -coverprofile=coverage.out ./...
|
||
go tool cover -html=coverage.out -o coverage.html
|
||
|
||
# パッケージごとのカバレッジを表示
|
||
go test -cover ./...
|
||
|
||
# 詳細なカバレッジ
|
||
go test -coverprofile=coverage.out -covermode=atomic ./...
|
||
```
|
||
|
||
### カバレッジのベストプラクティス
|
||
|
||
```go
|
||
// Good: テスタブルなコード
|
||
func ProcessData(data []byte) (Result, error) {
|
||
if len(data) == 0 {
|
||
return Result{}, ErrEmptyData
|
||
}
|
||
|
||
// 各分岐をテスト可能
|
||
if isValid(data) {
|
||
return parseValid(data)
|
||
}
|
||
return parseInvalid(data)
|
||
}
|
||
|
||
// 対応するテストが全分岐をカバー
|
||
func TestProcessData(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
data []byte
|
||
wantErr bool
|
||
}{
|
||
{"empty data", []byte{}, true},
|
||
{"valid data", []byte("valid"), false},
|
||
{"invalid data", []byte("invalid"), false},
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
## 統合テスト
|
||
|
||
### ビルドタグの使用
|
||
|
||
```go
|
||
//go:build integration
|
||
// +build integration
|
||
|
||
package myapp_test
|
||
|
||
import "testing"
|
||
|
||
func TestDatabaseIntegration(t *testing.T) {
|
||
// 実際のDBを必要とするテスト
|
||
}
|
||
```
|
||
|
||
```bash
|
||
# 統合テストを実行
|
||
go test -tags=integration ./...
|
||
|
||
# 統合テストを除外
|
||
go test ./...
|
||
```
|
||
|
||
### テストコンテナの使用
|
||
|
||
```go
|
||
import "github.com/testcontainers/testcontainers-go"
|
||
|
||
func setupPostgres(t *testing.T) *sql.DB {
|
||
ctx := context.Background()
|
||
|
||
req := testcontainers.ContainerRequest{
|
||
Image: "postgres:15",
|
||
ExposedPorts: []string{"5432/tcp"},
|
||
Env: map[string]string{
|
||
"POSTGRES_PASSWORD": "test",
|
||
"POSTGRES_DB": "testdb",
|
||
},
|
||
}
|
||
|
||
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||
ContainerRequest: req,
|
||
Started: true,
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
t.Cleanup(func() {
|
||
container.Terminate(ctx)
|
||
})
|
||
|
||
// コンテナに接続
|
||
// ...
|
||
return db
|
||
}
|
||
```
|
||
|
||
## テストの並列化
|
||
|
||
### 並列テスト
|
||
|
||
```go
|
||
func TestParallel(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
fn func(*testing.T)
|
||
}{
|
||
{"test1", testCase1},
|
||
{"test2", testCase2},
|
||
{"test3", testCase3},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
tt := tt // ループ変数をキャプチャ
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
t.Parallel() // このテストを並列実行
|
||
tt.fn(t)
|
||
})
|
||
}
|
||
}
|
||
```
|
||
|
||
### 並列実行の制御
|
||
|
||
```go
|
||
func TestWithResourceLimit(t *testing.T) {
|
||
// 同時に5つのテストのみ
|
||
sem := make(chan struct{}, 5)
|
||
|
||
tests := generateManyTests()
|
||
|
||
for _, tt := range tests {
|
||
tt := tt
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
sem <- struct{}{} // 獲得
|
||
defer func() { <-sem }() // 解放
|
||
|
||
tt.fn(t)
|
||
})
|
||
}
|
||
}
|
||
```
|
||
|
||
## Goツール統合
|
||
|
||
### テストコマンド
|
||
|
||
```bash
|
||
# 基本テスト
|
||
go test ./...
|
||
go test -v ./... # 詳細出力
|
||
go test -run TestSpecific ./... # 特定のテストを実行
|
||
|
||
# カバレッジ
|
||
go test -cover ./...
|
||
go test -coverprofile=coverage.out ./...
|
||
|
||
# レースコンディション
|
||
go test -race ./...
|
||
|
||
# ベンチマーク
|
||
go test -bench=. ./...
|
||
go test -bench=. -benchmem ./...
|
||
go test -bench=. -cpuprofile=cpu.prof ./...
|
||
|
||
# ファジング
|
||
go test -fuzz=FuzzTest
|
||
|
||
# 統合テスト
|
||
go test -tags=integration ./...
|
||
|
||
# JSONフォーマット(CI統合用)
|
||
go test -json ./...
|
||
```
|
||
|
||
### テスト設定
|
||
|
||
```bash
|
||
# テストタイムアウト
|
||
go test -timeout 30s ./...
|
||
|
||
# 短時間テスト(長時間テストをスキップ)
|
||
go test -short ./...
|
||
|
||
# ビルドキャッシュのクリア
|
||
go clean -testcache
|
||
go test ./...
|
||
```
|
||
|
||
## ベストプラクティス
|
||
|
||
### DRY(Don't Repeat Yourself)原則
|
||
|
||
```go
|
||
// Good: テーブル駆動テストで繰り返しを削減
|
||
func TestValidation(t *testing.T) {
|
||
tests := []struct {
|
||
input string
|
||
valid bool
|
||
}{
|
||
{"valid@email.com", true},
|
||
{"invalid-email", false},
|
||
{"", false},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.input, func(t *testing.T) {
|
||
err := Validate(tt.input)
|
||
if (err == nil) != tt.valid {
|
||
t.Errorf("Validate(%q) error = %v, want valid = %v",
|
||
tt.input, err, tt.valid)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
```
|
||
|
||
### テストデータの分離
|
||
|
||
```go
|
||
// Good: テストデータを testdata/ ディレクトリに配置
|
||
func TestLoadConfig(t *testing.T) {
|
||
data, err := os.ReadFile("testdata/config.json")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
config, err := ParseConfig(data)
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### クリーンアップの使用
|
||
|
||
```go
|
||
func TestWithCleanup(t *testing.T) {
|
||
// リソースを設定
|
||
file, err := os.CreateTemp("", "test")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
// クリーンアップを登録(deferに似ているが、サブテストで動作)
|
||
t.Cleanup(func() {
|
||
os.Remove(file.Name())
|
||
})
|
||
|
||
// テストを続ける...
|
||
}
|
||
```
|
||
|
||
### エラーメッセージの明確化
|
||
|
||
```go
|
||
// Bad: 不明確なエラー
|
||
if result != expected {
|
||
t.Error("wrong result")
|
||
}
|
||
|
||
// Good: コンテキスト付きエラー
|
||
if result != expected {
|
||
t.Errorf("Calculate(%d) = %d; want %d", input, result, expected)
|
||
}
|
||
|
||
// Better: ヘルパー関数の使用
|
||
assertEqual(t, result, expected, "Calculate(%d)", input)
|
||
```
|
||
|
||
## 避けるべきアンチパターン
|
||
|
||
```go
|
||
// Bad: 外部状態に依存
|
||
func TestBadDependency(t *testing.T) {
|
||
result := GetUserFromDatabase("123") // 実際のDBを使用
|
||
// テストが壊れやすく遅い
|
||
}
|
||
|
||
// Good: 依存を注入
|
||
func TestGoodDependency(t *testing.T) {
|
||
mockDB := &MockDatabase{
|
||
users: map[string]User{"123": {ID: "123"}},
|
||
}
|
||
result := GetUser(mockDB, "123")
|
||
}
|
||
|
||
// Bad: テスト間で状態を共有
|
||
var sharedCounter int
|
||
|
||
func TestShared1(t *testing.T) {
|
||
sharedCounter++
|
||
// テストの順序に依存
|
||
}
|
||
|
||
// Good: 各テストを独立させる
|
||
func TestIndependent(t *testing.T) {
|
||
counter := 0
|
||
counter++
|
||
// 他のテストに影響しない
|
||
}
|
||
|
||
// Bad: エラーを無視
|
||
func TestIgnoreError(t *testing.T) {
|
||
result, _ := Process()
|
||
if result != expected {
|
||
t.Error("wrong result")
|
||
}
|
||
}
|
||
|
||
// Good: エラーをチェック
|
||
func TestCheckError(t *testing.T) {
|
||
result, err := Process()
|
||
if err != nil {
|
||
t.Fatalf("Process() error = %v", err)
|
||
}
|
||
if result != expected {
|
||
t.Errorf("got %v, want %v", result, expected)
|
||
}
|
||
}
|
||
```
|
||
|
||
## クイックリファレンス
|
||
|
||
| コマンド/パターン | 目的 |
|
||
|--------------|---------|
|
||
| `go test ./...` | すべてのテストを実行 |
|
||
| `go test -v` | 詳細出力 |
|
||
| `go test -cover` | カバレッジレポート |
|
||
| `go test -race` | レースコンディション検出 |
|
||
| `go test -bench=.` | ベンチマークを実行 |
|
||
| `t.Run()` | サブテスト |
|
||
| `t.Helper()` | テストヘルパー関数 |
|
||
| `t.Parallel()` | テストを並列実行 |
|
||
| `t.Cleanup()` | クリーンアップを登録 |
|
||
| `testdata/` | テストフィクスチャ用ディレクトリ |
|
||
| `-short` | 長時間テストをスキップ |
|
||
| `-tags=integration` | ビルドタグでテストを実行 |
|
||
|
||
**覚えておいてください**: 良いテストは高速で、信頼性があり、保守可能で、明確です。複雑さより明確さを目指してください。
|