@vincent119/go-copilot-rules 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent_agy/INSTALLATION.md +786 -0
- package/.agent_agy/README.md +243 -0
- package/.agent_agy/SKILLS_INDEX.md +516 -0
- package/.agent_agy/rules/go-core.copilot-instructions.md +251 -0
- package/.agent_agy/skills/go-api-design/SKILL.md +535 -0
- package/.agent_agy/skills/go-ci-tooling/SKILL.md +533 -0
- package/.agent_agy/skills/go-configuration/SKILL.md +609 -0
- package/.agent_agy/skills/go-database/SKILL.md +412 -0
- package/.agent_agy/skills/go-ddd/SKILL.md +374 -0
- package/.agent_agy/skills/go-dependency-injection/SKILL.md +546 -0
- package/.agent_agy/skills/go-domain-events/SKILL.md +525 -0
- package/.agent_agy/skills/go-examples/SKILL.md +690 -0
- package/.agent_agy/skills/go-graceful-shutdown/SKILL.md +708 -0
- package/.agent_agy/skills/go-grpc/SKILL.md +484 -0
- package/.agent_agy/skills/go-http-advanced/SKILL.md +494 -0
- package/.agent_agy/skills/go-observability/SKILL.md +684 -0
- package/.agent_agy/skills/go-testing-advanced/SKILL.md +573 -0
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/cli/install.js +344 -0
- package/package.json +47 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: go-testing-advanced
|
|
3
|
+
description: |
|
|
4
|
+
Go 進階測試策略:Table-driven tests 進階模式、Mocking 策略(uber-go/mock)、
|
|
5
|
+
整合測試設計、Benchmark 與 Fuzz testing、測試覆蓋率要求、測試金字塔原則。
|
|
6
|
+
|
|
7
|
+
**適用場景**:撰寫單元測試、設計 Mock、實作整合測試、效能測試(Benchmark)、
|
|
8
|
+
模糊測試(Fuzz)、提升測試覆蓋率、測試 Repository/Use Case。
|
|
9
|
+
|
|
10
|
+
**關鍵字**:testing, unit test, integration test, mock, gomock, uber-go/mock, mockery,
|
|
11
|
+
table driven test, benchmark, fuzz testing, test coverage, testify, test fixtures,
|
|
12
|
+
test doubles, test pyramid, t.Helper, t.Cleanup, t.Context
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Go 進階測試策略
|
|
16
|
+
|
|
17
|
+
> **相關 Skills**:本規範建議搭配 `go-dependency-injection`(測試 DI 容器配置)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Table-Driven Tests 進階模式
|
|
22
|
+
|
|
23
|
+
### 基礎模式
|
|
24
|
+
|
|
25
|
+
```go
|
|
26
|
+
func TestAdd(t *testing.T) {
|
|
27
|
+
tests := []struct {
|
|
28
|
+
name string
|
|
29
|
+
a, b int
|
|
30
|
+
expected int
|
|
31
|
+
}{
|
|
32
|
+
{name: "positive numbers", a: 2, b: 3, expected: 5},
|
|
33
|
+
{name: "negative numbers", a: -1, b: -2, expected: -3},
|
|
34
|
+
{name: "zero", a: 0, b: 0, expected: 0},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for _, tt := range tests {
|
|
38
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
39
|
+
result := Add(tt.a, tt.b)
|
|
40
|
+
if result != tt.expected {
|
|
41
|
+
t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, result, tt.expected)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 進階模式:測試錯誤情境
|
|
49
|
+
|
|
50
|
+
```go
|
|
51
|
+
func TestProcessData(t *testing.T) {
|
|
52
|
+
tests := []struct {
|
|
53
|
+
name string
|
|
54
|
+
input []byte
|
|
55
|
+
wantErr bool
|
|
56
|
+
errTarget error // 使用 errors.Is 檢查
|
|
57
|
+
}{
|
|
58
|
+
{
|
|
59
|
+
name: "valid data",
|
|
60
|
+
input: []byte(`{"key":"value"}`),
|
|
61
|
+
wantErr: false,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "invalid json",
|
|
65
|
+
input: []byte(`{invalid}`),
|
|
66
|
+
wantErr: true,
|
|
67
|
+
errTarget: ErrInvalidJSON,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "empty data",
|
|
71
|
+
input: nil,
|
|
72
|
+
wantErr: true,
|
|
73
|
+
errTarget: ErrEmptyData,
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for _, tt := range tests {
|
|
78
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
79
|
+
err := ProcessData(tt.input)
|
|
80
|
+
|
|
81
|
+
if tt.wantErr {
|
|
82
|
+
if err == nil {
|
|
83
|
+
t.Fatal("expected error, got nil")
|
|
84
|
+
}
|
|
85
|
+
if tt.errTarget != nil && !errors.Is(err, tt.errTarget) {
|
|
86
|
+
t.Errorf("expected error %v, got %v", tt.errTarget, err)
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
if err != nil {
|
|
90
|
+
t.Errorf("unexpected error: %v", err)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 使用 t.Helper() 簡化斷言
|
|
99
|
+
|
|
100
|
+
```go
|
|
101
|
+
// 測試輔助函式
|
|
102
|
+
func assertNoError(t *testing.T, err error) {
|
|
103
|
+
t.Helper() // 標記為輔助函式,錯誤會指向呼叫者行號
|
|
104
|
+
if err != nil {
|
|
105
|
+
t.Fatalf("unexpected error: %v", err)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func assertEqual(t *testing.T, got, want interface{}) {
|
|
110
|
+
t.Helper()
|
|
111
|
+
if got != want {
|
|
112
|
+
t.Errorf("got %v, want %v", got, want)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 使用範例
|
|
117
|
+
func TestMyFunction(t *testing.T) {
|
|
118
|
+
result, err := MyFunction()
|
|
119
|
+
assertNoError(t, err)
|
|
120
|
+
assertEqual(t, result, "expected")
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Mocking 策略(uber-go/mock)
|
|
127
|
+
|
|
128
|
+
### 安裝與配置
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# 安裝 uber-go/mock(原 gomock)
|
|
132
|
+
go install go.uber.org/mock/mockgen@latest
|
|
133
|
+
|
|
134
|
+
# 或使用 mockery
|
|
135
|
+
go install github.com/vektra/mockery/v2@latest
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Interface 設計原則
|
|
139
|
+
|
|
140
|
+
**原則**:
|
|
141
|
+
- Repository / Service 皆以 **interface** 暴露;實作為 private struct
|
|
142
|
+
- Interface 定義於 Domain 或 Application 層,實作於 Infra 層
|
|
143
|
+
|
|
144
|
+
**範例**:
|
|
145
|
+
```go
|
|
146
|
+
// internal/order/domain/repository.go
|
|
147
|
+
package domain
|
|
148
|
+
|
|
149
|
+
//go:generate mockgen -source=repository.go -destination=../../mocks/order_repository_mock.go -package=mocks
|
|
150
|
+
type Repository interface {
|
|
151
|
+
Save(ctx context.Context, order *Order) error
|
|
152
|
+
FindByID(ctx context.Context, id string) (*Order, error)
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 產生 Mock
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# 在專案根目錄執行
|
|
160
|
+
go generate ./...
|
|
161
|
+
|
|
162
|
+
# 或手動產生
|
|
163
|
+
mockgen -source=internal/order/domain/repository.go \
|
|
164
|
+
-destination=internal/mocks/order_repository_mock.go \
|
|
165
|
+
-package=mocks
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**目錄結構**:
|
|
169
|
+
```
|
|
170
|
+
internal/
|
|
171
|
+
├── order/
|
|
172
|
+
│ └── domain/
|
|
173
|
+
│ └── repository.go
|
|
174
|
+
└── mocks/
|
|
175
|
+
└── order_repository_mock.go # 統一放置 Mock
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### 使用 Mock 撰寫單元測試
|
|
179
|
+
|
|
180
|
+
```go
|
|
181
|
+
// internal/order/application/create_order_usecase_test.go
|
|
182
|
+
package application_test
|
|
183
|
+
|
|
184
|
+
import (
|
|
185
|
+
"context"
|
|
186
|
+
"testing"
|
|
187
|
+
|
|
188
|
+
"go.uber.org/mock/gomock"
|
|
189
|
+
"myapp/internal/mocks"
|
|
190
|
+
"myapp/internal/order/application"
|
|
191
|
+
"myapp/internal/order/domain"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
func TestCreateOrderUseCase_Execute(t *testing.T) {
|
|
195
|
+
tests := []struct {
|
|
196
|
+
name string
|
|
197
|
+
setup func(*mocks.MockRepository)
|
|
198
|
+
input application.CreateOrderInput
|
|
199
|
+
wantErr bool
|
|
200
|
+
}{
|
|
201
|
+
{
|
|
202
|
+
name: "success",
|
|
203
|
+
setup: func(m *mocks.MockRepository) {
|
|
204
|
+
// 設定 Mock 預期行為
|
|
205
|
+
m.EXPECT().
|
|
206
|
+
Save(gomock.Any(), gomock.Any()).
|
|
207
|
+
Return(nil).
|
|
208
|
+
Times(1)
|
|
209
|
+
},
|
|
210
|
+
input: application.CreateOrderInput{
|
|
211
|
+
CustomerID: "cust-123",
|
|
212
|
+
Items: []domain.OrderItem{{ProductID: "prod-1", Quantity: 2}},
|
|
213
|
+
},
|
|
214
|
+
wantErr: false,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "repository error",
|
|
218
|
+
setup: func(m *mocks.MockRepository) {
|
|
219
|
+
m.EXPECT().
|
|
220
|
+
Save(gomock.Any(), gomock.Any()).
|
|
221
|
+
Return(domain.ErrDatabaseError).
|
|
222
|
+
Times(1)
|
|
223
|
+
},
|
|
224
|
+
input: application.CreateOrderInput{
|
|
225
|
+
CustomerID: "cust-123",
|
|
226
|
+
Items: []domain.OrderItem{{ProductID: "prod-1", Quantity: 2}},
|
|
227
|
+
},
|
|
228
|
+
wantErr: true,
|
|
229
|
+
},
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for _, tt := range tests {
|
|
233
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
234
|
+
// Arrange
|
|
235
|
+
ctrl := gomock.NewController(t)
|
|
236
|
+
defer ctrl.Finish()
|
|
237
|
+
|
|
238
|
+
mockRepo := mocks.NewMockRepository(ctrl)
|
|
239
|
+
tt.setup(mockRepo)
|
|
240
|
+
|
|
241
|
+
uc := application.NewCreateOrderUseCase(mockRepo)
|
|
242
|
+
|
|
243
|
+
// Act
|
|
244
|
+
err := uc.Execute(t.Context(), tt.input) // Go 1.24+ 使用 t.Context()
|
|
245
|
+
|
|
246
|
+
// Assert
|
|
247
|
+
if (err != nil) != tt.wantErr {
|
|
248
|
+
t.Errorf("Execute() error = %v, wantErr %v", err, tt.wantErr)
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### 進階 Mock 技巧
|
|
256
|
+
|
|
257
|
+
#### 1. 驗證參數內容
|
|
258
|
+
|
|
259
|
+
```go
|
|
260
|
+
m.EXPECT().
|
|
261
|
+
Save(gomock.Any(), gomock.Cond(func(x interface{}) bool {
|
|
262
|
+
order, ok := x.(*domain.Order)
|
|
263
|
+
return ok && order.CustomerID == "expected-id"
|
|
264
|
+
})).
|
|
265
|
+
Return(nil)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
#### 2. 模擬多次呼叫
|
|
269
|
+
|
|
270
|
+
```go
|
|
271
|
+
m.EXPECT().FindByID(gomock.Any(), "id-1").Return(&domain.Order{}, nil)
|
|
272
|
+
m.EXPECT().FindByID(gomock.Any(), "id-2").Return(nil, domain.ErrNotFound)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
#### 3. 使用 gomock.InOrder 保證順序
|
|
276
|
+
|
|
277
|
+
```go
|
|
278
|
+
gomock.InOrder(
|
|
279
|
+
m.EXPECT().Save(gomock.Any(), gomock.Any()).Return(nil),
|
|
280
|
+
m.EXPECT().FindByID(gomock.Any(), "id").Return(&domain.Order{}, nil),
|
|
281
|
+
)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## 整合測試設計
|
|
287
|
+
|
|
288
|
+
### 測試金字塔原則
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
╱╲
|
|
292
|
+
╱ ╲ E2E Tests (10%)
|
|
293
|
+
╱────╲
|
|
294
|
+
╱ ╲ Integration Tests (30%)
|
|
295
|
+
╱────────╲
|
|
296
|
+
╱ ╲ Unit Tests (60%)
|
|
297
|
+
────────────
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### 整合測試範例(使用真實 DB)
|
|
301
|
+
|
|
302
|
+
```go
|
|
303
|
+
// internal/order/infra/order_repository_integration_test.go
|
|
304
|
+
// +build integration
|
|
305
|
+
|
|
306
|
+
package infra_test
|
|
307
|
+
|
|
308
|
+
import (
|
|
309
|
+
"context"
|
|
310
|
+
"testing"
|
|
311
|
+
|
|
312
|
+
"github.com/testcontainers/testcontainers-go"
|
|
313
|
+
"github.com/testcontainers/testcontainers-go/wait"
|
|
314
|
+
"gorm.io/driver/postgres"
|
|
315
|
+
"gorm.io/gorm"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
func setupTestDB(t *testing.T) *gorm.DB {
|
|
319
|
+
t.Helper()
|
|
320
|
+
|
|
321
|
+
// 使用 testcontainers 啟動 PostgreSQL
|
|
322
|
+
ctx := context.Background()
|
|
323
|
+
req := testcontainers.ContainerRequest{
|
|
324
|
+
Image: "postgres:15-alpine",
|
|
325
|
+
ExposedPorts: []string{"5432/tcp"},
|
|
326
|
+
Env: map[string]string{
|
|
327
|
+
"POSTGRES_PASSWORD": "test",
|
|
328
|
+
"POSTGRES_DB": "testdb",
|
|
329
|
+
},
|
|
330
|
+
WaitingFor: wait.ForListeningPort("5432/tcp"),
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
|
334
|
+
ContainerRequest: req,
|
|
335
|
+
Started: true,
|
|
336
|
+
})
|
|
337
|
+
if err != nil {
|
|
338
|
+
t.Fatalf("failed to start container: %v", err)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
t.Cleanup(func() {
|
|
342
|
+
_ = container.Terminate(ctx)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
// 取得連線資訊
|
|
346
|
+
host, _ := container.Host(ctx)
|
|
347
|
+
port, _ := container.MappedPort(ctx, "5432")
|
|
348
|
+
|
|
349
|
+
dsn := fmt.Sprintf("host=%s port=%s user=postgres password=test dbname=testdb sslmode=disable",
|
|
350
|
+
host, port.Port())
|
|
351
|
+
|
|
352
|
+
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
|
353
|
+
if err != nil {
|
|
354
|
+
t.Fatalf("failed to connect db: %v", err)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 執行 Migration
|
|
358
|
+
_ = db.AutoMigrate(&OrderModel{})
|
|
359
|
+
|
|
360
|
+
return db
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
func TestOrderRepository_Integration(t *testing.T) {
|
|
364
|
+
db := setupTestDB(t)
|
|
365
|
+
repo := infra.NewOrderRepository(db)
|
|
366
|
+
|
|
367
|
+
t.Run("save and find", func(t *testing.T) {
|
|
368
|
+
ctx := context.Background()
|
|
369
|
+
order := domain.NewOrder("cust-123", []domain.OrderItem{})
|
|
370
|
+
|
|
371
|
+
// Save
|
|
372
|
+
err := repo.Save(ctx, order)
|
|
373
|
+
if err != nil {
|
|
374
|
+
t.Fatalf("Save() error: %v", err)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Find
|
|
378
|
+
found, err := repo.FindByID(ctx, order.ID())
|
|
379
|
+
if err != nil {
|
|
380
|
+
t.Fatalf("FindByID() error: %v", err)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if found.ID() != order.ID() {
|
|
384
|
+
t.Errorf("ID mismatch: got %s, want %s", found.ID(), order.ID())
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### 執行整合測試
|
|
391
|
+
|
|
392
|
+
```bash
|
|
393
|
+
# 僅執行整合測試
|
|
394
|
+
go test -tags=integration ./...
|
|
395
|
+
|
|
396
|
+
# 排除整合測試(預設)
|
|
397
|
+
go test ./...
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## Benchmark(效能測試)
|
|
403
|
+
|
|
404
|
+
### 基礎 Benchmark
|
|
405
|
+
|
|
406
|
+
```go
|
|
407
|
+
func BenchmarkProcessData(b *testing.B) {
|
|
408
|
+
data := generateTestData()
|
|
409
|
+
|
|
410
|
+
// 重置計時器(排除準備時間)
|
|
411
|
+
b.ResetTimer()
|
|
412
|
+
|
|
413
|
+
for i := 0; i < b.N; i++ {
|
|
414
|
+
ProcessData(data)
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### 並行 Benchmark
|
|
420
|
+
|
|
421
|
+
```go
|
|
422
|
+
func BenchmarkProcessDataParallel(b *testing.B) {
|
|
423
|
+
data := generateTestData()
|
|
424
|
+
|
|
425
|
+
b.RunParallel(func(pb *testing.PB) {
|
|
426
|
+
for pb.Next() {
|
|
427
|
+
ProcessData(data)
|
|
428
|
+
}
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### 報告記憶體分配
|
|
434
|
+
|
|
435
|
+
```go
|
|
436
|
+
func BenchmarkEncodeJSON(b *testing.B) {
|
|
437
|
+
obj := &MyStruct{Field: "value"}
|
|
438
|
+
|
|
439
|
+
b.ReportAllocs() // 報告記憶體分配
|
|
440
|
+
b.ResetTimer()
|
|
441
|
+
|
|
442
|
+
for i := 0; i < b.N; i++ {
|
|
443
|
+
_, _ = json.Marshal(obj)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### 執行 Benchmark
|
|
449
|
+
|
|
450
|
+
```bash
|
|
451
|
+
# 執行所有 Benchmark
|
|
452
|
+
go test -bench=. -benchmem ./...
|
|
453
|
+
|
|
454
|
+
# 執行特定 Benchmark
|
|
455
|
+
go test -bench=BenchmarkProcessData -benchmem
|
|
456
|
+
|
|
457
|
+
# 比較兩次結果
|
|
458
|
+
go test -bench=. -benchmem > old.txt
|
|
459
|
+
# ...程式碼修改...
|
|
460
|
+
go test -bench=. -benchmem > new.txt
|
|
461
|
+
benchstat old.txt new.txt
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Fuzz Testing(模糊測試)
|
|
467
|
+
|
|
468
|
+
### 基礎 Fuzz Test(Go 1.18+)
|
|
469
|
+
|
|
470
|
+
```go
|
|
471
|
+
func FuzzParseInput(f *testing.F) {
|
|
472
|
+
// Seed corpus(種子資料)
|
|
473
|
+
f.Add("valid input")
|
|
474
|
+
f.Add("123")
|
|
475
|
+
f.Add("")
|
|
476
|
+
|
|
477
|
+
f.Fuzz(func(t *testing.T, input string) {
|
|
478
|
+
// 測試不應 panic
|
|
479
|
+
result, err := ParseInput(input)
|
|
480
|
+
|
|
481
|
+
// 驗證錯誤處理
|
|
482
|
+
if err != nil {
|
|
483
|
+
// 錯誤是預期的,但不應 panic
|
|
484
|
+
return
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// 驗證結果屬性
|
|
488
|
+
if len(result) > 0 && result[0] == 0 {
|
|
489
|
+
t.Errorf("unexpected zero value in result")
|
|
490
|
+
}
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### 執行 Fuzz Test
|
|
496
|
+
|
|
497
|
+
```bash
|
|
498
|
+
# 執行 Fuzz Test(預設 1 分鐘)
|
|
499
|
+
go test -fuzz=FuzzParseInput
|
|
500
|
+
|
|
501
|
+
# 指定執行時間
|
|
502
|
+
go test -fuzz=FuzzParseInput -fuzztime=10m
|
|
503
|
+
|
|
504
|
+
# Fuzz 發現的問題會存入 testdata/fuzz/
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## 測試覆蓋率
|
|
510
|
+
|
|
511
|
+
### 產生覆蓋率報告
|
|
512
|
+
|
|
513
|
+
```bash
|
|
514
|
+
# 產生覆蓋率檔案
|
|
515
|
+
go test -coverprofile=coverage.out ./...
|
|
516
|
+
|
|
517
|
+
# 查看總覆蓋率
|
|
518
|
+
go tool cover -func=coverage.out
|
|
519
|
+
|
|
520
|
+
# 產生 HTML 報告
|
|
521
|
+
go tool cover -html=coverage.out -o coverage.html
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### 覆蓋率要求
|
|
525
|
+
|
|
526
|
+
- **單元測試覆蓋率**:≥ 80%(可透過 PR 調整)
|
|
527
|
+
- **核心業務邏輯**:≥ 90%
|
|
528
|
+
- **整合測試**:涵蓋關鍵路徑與錯誤情境
|
|
529
|
+
|
|
530
|
+
### CI 整合
|
|
531
|
+
|
|
532
|
+
```yaml
|
|
533
|
+
# .github/workflows/test.yml
|
|
534
|
+
- name: Run tests with coverage
|
|
535
|
+
run: |
|
|
536
|
+
go test -race -coverprofile=coverage.out -covermode=atomic ./...
|
|
537
|
+
go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//' | \
|
|
538
|
+
awk '{if ($1 < 80) exit 1}'
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
## 檢查清單
|
|
544
|
+
|
|
545
|
+
**單元測試**
|
|
546
|
+
- [ ] 使用 table-driven tests
|
|
547
|
+
- [ ] 使用 `t.Run` 建立子測試
|
|
548
|
+
- [ ] 使用 `t.Helper()` 標記輔助函式
|
|
549
|
+
- [ ] 使用 `t.Cleanup()` 清理資源
|
|
550
|
+
- [ ] 使用 `t.Context()` 獲取 context(Go 1.24+)
|
|
551
|
+
- [ ] Mock 統一放置於 `internal/mocks/`
|
|
552
|
+
- [ ] 使用 `go generate` 自動產生 Mock
|
|
553
|
+
|
|
554
|
+
**整合測試**
|
|
555
|
+
- [ ] 使用 build tags(`// +build integration`)
|
|
556
|
+
- [ ] 使用 testcontainers 或測試資料庫
|
|
557
|
+
- [ ] 測試真實的 DB/Cache 互動
|
|
558
|
+
- [ ] 執行 Migration 初始化 Schema
|
|
559
|
+
|
|
560
|
+
**Benchmark**
|
|
561
|
+
- [ ] 使用 `b.ResetTimer()` 排除準備時間
|
|
562
|
+
- [ ] 使用 `b.ReportAllocs()` 報告記憶體
|
|
563
|
+
- [ ] 關鍵路徑提供 Benchmark
|
|
564
|
+
|
|
565
|
+
**Fuzz Testing**
|
|
566
|
+
- [ ] 輸入解析函式提供 Fuzz Test
|
|
567
|
+
- [ ] 驗證不會 panic
|
|
568
|
+
- [ ] 種子資料涵蓋邊界情況
|
|
569
|
+
|
|
570
|
+
**覆蓋率**
|
|
571
|
+
- [ ] 單元測試覆蓋率 ≥ 80%
|
|
572
|
+
- [ ] CI 自動檢查覆蓋率門檻
|
|
573
|
+
- [ ] 核心業務邏輯覆蓋率 ≥ 90%
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|