Skip to main content

golang-testing

Go testing patterns including table-driven tests, subtests, benchmarks, fuzzing, and test coverage. Follows TDD methodology with idiomatic Go practices. Use when this capability is needed.

Informations de source

Dépôt
tomevault-io/tomes
Dernière activité de la source
23 juillet 2026 à 21:48
Langue détectée de SKILL.md
chinois
Étoiles
1
Forks
0

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
golang-testing
description
Go testing patterns including table-driven tests, subtests, benchmarks, fuzzing, and test coverage. Follows TDD methodology with idiomatic Go practices. Use when this capability is needed.
metadata
{"author":"codelably"}
# Go 測試模式 用於撰寫可靠、可維護測試的完整 Go 測試模式,遵循 TDD 方法論。 ## 何時啟用 - 撰寫新的 Go 函式或方法 - 為現有程式碼增加測試覆蓋率 - 為效能關鍵程式碼建立基準測試 - 實作輸入驗證的模糊測試 - 在 Go 專案中遵循 TDD 工作流程 ## Go 的 TDD 工作流程 ### RED-GREEN-REFACTOR 循環 ``` RED → 先寫失敗的測試 GREEN → 撰寫最少程式碼使測試通過 REFACTOR → 在保持測試綠色的同時改善程式碼 REPEAT → 繼續下一個需求 ``` ### Go 中的逐步 TDD ```go // 步驟 1:定義介面/簽章 // calculator.go package calculator func Add(a, b int) int { panic("not implemented") // 佔位符 } // 步驟 2:撰寫失敗測試(RED) // calculator_test.go package calculator import "testing" func TestAdd(t *testing.T) { got := Add(2, 3) want := 5 if got != want { t.Errorf("Add(2, 3) = %d; want %d", got, want) } } // 步驟 3:執行測試 - 驗證失敗 // $ go test // --- FAIL: TestAdd (0.00s) // panic: not implemented // 步驟 4:實作最少程式碼(GREEN) func Add(a, b int) int { return a + b } // 步驟 5:執行測試 - 驗證通過 // $ go test // PASS // 步驟 6:如需要則重構,驗證測試仍然通過 ``` ## 表格驅動測試 Go 測試的標準模式。以最少程式碼達到完整覆蓋。 ```go func TestAdd(t *testing.T) { tests := []struct { name string a, b int expected int }{ {"positive numbers", 2, 3, 5}, {"negative numbers", -1, -2, -3}, {"zero values", 0, 0, 0}, {"mixed signs", -1, 1, 0}, {"large numbers", 1000000, 2000000, 3000000}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := Add(tt.a, tt.b) if got != tt.expected { t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.expected) } }) } } ``` ### 帶錯誤案例的表格驅動測試 ```go func TestParseConfig(t *testing.T) { tests := []struct { name string input string want *Config wantErr bool }{ { name: "valid config", input: `{"host": "localhost", "port": 8080}`, want: &Config{Host: "localhost", Port: 8080}, }, { name: "invalid JSON", input: `{invalid}`, wantErr: true, }, { name: "empty input", input: "", wantErr: true, }, { name: "minimal config", input: `{}`, want: &Config{}, // 零值 config }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := ParseConfig(tt.input) if tt.wantErr { if err == nil { t.Error("expected error, got nil") } return } if err != nil { t.Fatalf("unexpected error: %v", err) } if !reflect.DeepEqual(got, tt.want) { t.Errorf("got %+v; want %+v", got, tt.want) } }) } } ``` ## 子測試 ### 組織相關測試 ```go func TestUser(t *testing.T) { // 所有子測試共享的設置 db := setupTestDB(t) t.Run("Create", func(t *testing.T) { user := &User{Name: "Alice"} err := db.CreateUser(user) if err != nil { t.Fatalf("CreateUser failed: %v", err) } if user.ID == "" { t.Error("expected user ID to be set") } }) t.Run("Get", func(t *testing.T) { user, err := db.GetUser("alice-id") if err != nil { t.Fatalf("GetUser failed: %v", err) } if user.Name != "Alice" { t.Errorf("got name %q; want %q", user.Name, "Alice") } }) t.Run("Update", func(t *testing.T) { // ... }) t.Run("Delete", func(t *testing.T) { // ... }) } ``` ### 並行子測試 ```go func TestParallel(t *testing.T) { tests := []struct { name string input string }{ {"case1", "input1"}, {"case2", "input2"}, {"case3", "input3"}, } for _, tt := range tests { tt := tt // 捕獲範圍變數 t.Run(tt.name, func(t *testing.T) { t.Parallel() // 並行執行子測試 result := Process(tt.input) // 斷言... _ = result }) } } ``` ## 測試輔助函式 ### 輔助函式 ```go func setupTestDB(t *testing.T) *sql.DB { t.Helper() // 標記為輔助函式 db, err := sql.Open("sqlite3", ":memory:") if err != nil { t.Fatalf("failed to open database: %v", err) } // 測試結束時清理 t.Cleanup(func() { db.Close() }) // 執行 migrations if _, err := db.Exec(schema); err != nil { t.Fatalf("failed to create schema: %v", err) } return db } func assertNoError(t *testing.T, err error) { t.Helper() if err != nil { t.Fatalf("unexpected error: %v", err) } } func assertEqual[T comparable](t *testing.T, got, want T) { t.Helper() if got != want { t.Errorf("got %v; want %v", got, want) } } ``` ### 臨時檔案和目錄 ```go func TestFileProcessing(t *testing.T) { // 建立臨時目錄 - 自動清理 tmpDir := t.TempDir() // 建立測試檔案 testFile := filepath.Join(tmpDir, "test.txt") err := os.WriteFile(testFile, []byte("test content"), 0644) if err != nil { t.Fatalf("failed to create test file: %v", err) } // 執行測試 result, err := ProcessFile(testFile) if err != nil { t.Fatalf("ProcessFile failed: %v", err) } // 斷言... _ = result } ``` ## Golden 檔案 使用儲存在 `testdata/` 中的預期輸出檔案進行測試。 ```go var update = flag.Bool("update", false, "update golden files") func TestRender(t *testing.T) { tests := []struct { name string input Template }{ {"simple", Template{Name: "test"}}, {"complex", Template{Name: "test", Items: []string{"a", "b"}}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := Render(tt.input) golden := filepath.Join("testdata", tt.name+".golden") if *update { // 更新 golden 檔案:go test -update err := os.WriteFile(golden, got, 0644) if err != nil { t.Fatalf("failed to update golden file: %v", err) } } want, err := os.ReadFile(golden) if err != nil { t.Fatalf("failed to read golden file: %v", err) } if !bytes.Equal(got, want) { t.Errorf("output mismatch:\ngot:\n%s\nwant:\n%s", got, want) } }) } } ``` ## 使用介面 Mock ### 基於介面的 Mock ```go // 定義依賴的介面 type UserRepository interface { GetUser(id string) (*User, error) SaveUser(user *User) error } // 生產實作 type PostgresUserRepository struct { db *sql.DB } func (r *PostgresUserRepository) GetUser(id string) (*User, error) { // 實際資料庫查詢 } // 測試用 Mock 實作 type MockUserRepository struct { GetUserFunc func(id string) (*User, error) SaveUserFunc func(user *User) error } func (m *MockUserRepository) GetUser(id string) (*User, error) { return m.GetUserFunc(id) } func (m *MockUserRepository) SaveUser(user *User) error { return m.SaveUserFunc(user) } // 使用 mock 的測試 func TestUserService(t *testing.T) { mock := &MockUserRepository{ GetUserFunc: func(id string) (*User, error) { if id == "123" { return &User{ID: "123", Name: "Alice"}, nil } return nil, ErrNotFound }, } service := NewUserService(mock) user, err := service.GetUserProfile("123") if err != nil { t.Fatalf("unexpected error: %v", err) } if user.Name != "Alice" { t.Errorf("got name %q; want %q", user.Name, "Alice") } } ``` ## 基準測試 ### 基本基準測試 ```go func BenchmarkProcess(b *testing.B) { data := generateTestData(1000) b.ResetTimer() // 不計算設置時間 for i := 0; i < b.N; i++ { Process(data) } } // 執行:go test -bench=BenchmarkProcess -benchmem // 輸出:BenchmarkProcess-8 10000 105234 ns/op 4096 B/op 10 allocs/op ``` ### 不同大小的基準測試 ```go func BenchmarkSort(b *testing.B) { sizes := []int{100, 1000, 10000, 100000} for _, size := range sizes { b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { data := generateRandomSlice(size) b.ResetTimer() for i := 0; i < b.N; i++ { // 複製以避免排序已排序的資料 tmp := make([]int, len(data)) copy(tmp, data) sort.Ints(tmp) } }) } } ``` ### 記憶體分配基準測試 ```go func BenchmarkStringConcat(b *testing.B) { parts := []string{"hello", "world", "foo", "bar", "baz"} b.Run("plus", func(b *testing.B) { for i := 0; i < b.N; i++ { var s string for _, p := range parts { s += p } _ = s } }) b.Run("builder", func(b *testing.B) { for i := 0; i < b.N; i++ { var sb strings.Builder for _, p := range parts { sb.WriteString(p) } _ = sb.String() } }) b.Run("join", func(b *testing.B) { for i := 0; i < b.N; i++ { _ = strings.Join(parts, "") } }) } ``` ## 模糊測試(Go 1.18+) ### 基本模糊測試 ```go func FuzzParseJSON(f *testing.F) { // 新增種子語料庫 f.Add(`{"name": "test"}`) f.Add(`{"count": 123}`) f.Add(`[]`) f.Add(`""`)
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub