mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-15 06:23:28 +08:00
Revert "feat(ecc): prune plugin 43→12 items, promote 7 rules to .claude/rules/ (#245)"
This reverts commit 1bd68ff534.
This commit is contained in:
959
docs/ja-JP/skills/golang-testing/SKILL.md
Normal file
959
docs/ja-JP/skills/golang-testing/SKILL.md
Normal file
@@ -0,0 +1,959 @@
|
||||
---
|
||||
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` | ビルドタグでテストを実行 |
|
||||
|
||||
**覚えておいてください**: 良いテストは高速で、信頼性があり、保守可能で、明確です。複雑さより明確さを目指してください。
|
||||
Reference in New Issue
Block a user