本文 AI 產出,尚未審核
Golang – 測試與除錯
單元:整合測試與模擬(mock)
簡介
在大型 Go 專案中,單元測試 能保證每個函式在獨立環境下正確執行,而 整合測試 則驗證多個元件協同工作的行為。當測試對象依賴外部資源(資料庫、HTTP API、訊息佇列…)時,直接呼叫真實服務會讓測試變慢、難以重現,甚至產生副作用。這時 模擬(mock) 就派上用場:它以「假」的實作取代真實依賴,讓測試只關注核心邏輯。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步在 Go 中完成 整合測試 + mock,讓測試更快、更可靠,也更符合 CI/CD 流程的需求。
核心概念
1. 介面(interface)是 Mock 的基礎
Go 的介面抽象了行為,讓不同的實作可以互相替換。要對外部依賴做 mock,第一步就是 為依賴定義介面,然後在程式碼中只依賴介面而非具體型別。
// user_repo.go
type UserRepository interface {
FindByID(ctx context.Context, id int64) (*User, error)
Create(ctx context.Context, u *User) error
}
實際的資料庫實作會滿足 UserRepository,測試時則提供一個「假」的實作。
2. 手寫 Mock vs. 產生工具
- 手寫 Mock:簡單、可讀性高,適合依賴較少的介面。
- 自動產生:如
gomock、moq、mockery,可以根據介面自動產生大量樣板程式碼,減少手動錯誤。
以下示範使用官方的 testing 包手寫一個簡易 mock,接著再展示 gomock 的使用方式。
3. 測試套件結構建議
/project
│
├─ internal/
│ ├─ user/
│ │ ├─ service.go // 业务逻辑
│ │ ├─ repository.go // 介面定義
│ │ └─ repository_impl.go// 真實 DB 實作
│ └─ ...
│
├─ mock/
│ └─ user_repository_mock.go // 手寫或產生的 mock
│
└─ service_test.go // 整合測試
程式碼範例
範例 1️⃣ 手寫 Mock
// mock/user_repository_mock.go
type UserRepositoryMock struct {
// 用 map 來模擬資料庫
store map[int64]*User
// 用於驗證呼叫次數
FindCalls int
CreateCalls int
}
func NewUserRepositoryMock() *UserRepositoryMock {
return &UserRepositoryMock{store: make(map[int64]*User)}
}
func (m *UserRepositoryMock) FindByID(ctx context.Context, id int64) (*User, error) {
m.FindCalls++
if u, ok := m.store[id]; ok {
return u, nil
}
return nil, fmt.Errorf("user not found")
}
func (m *UserRepositoryMock) Create(ctx context.Context, u *User) error {
m.CreateCalls++
if _, exists := m.store[u.ID]; exists {
return fmt.Errorf("duplicate user")
}
m.store[u.ID] = u
return nil
}
測試中使用:
func TestService_CreateUser(t *testing.T) {
mockRepo := NewUserRepositoryMock()
svc := NewUserService(mockRepo) // 只依賴介面
user := &User{ID: 1, Name: "Alice"}
if err := svc.Register(context.Background(), user); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if mockRepo.CreateCalls != 1 {
t.Errorf("expected Create to be called once, got %d", mockRepo.CreateCalls)
}
}
範例 2️⃣ 使用 gomock 自動產生 Mock
- 安裝套件
go get github.com/golang/mock/gomock
go install github.com/golang/mock/mockgen@latest
- 產生 Mock
mockgen -source=internal/user/repository.go -destination=mock/user_repository_mock.go -package=mock
- 測試程式
func TestService_FindUser(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mock.NewMockUserRepository(ctrl)
// 設定期望:FindByID 會被呼叫一次,且回傳指定結果
mockRepo.
EXPECT().
FindByID(gomock.Any(), int64(42)).
Return(&User{ID: 42, Name: "Bob"}, nil).
Times(1)
svc := NewUserService(mockRepo)
user, err := svc.GetProfile(context.Background(), 42)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if user.Name != "Bob" {
t.Errorf("expected name Bob, got %s", user.Name)
}
}
範例 3️⃣ Mock HTTP Client
當服務需要呼叫外部 REST API 時,最常見的做法是 介面化 http.Client,或直接使用 httptest 產生測試伺服器。
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
// 真實實作
type RealHTTPClient struct {
client *http.Client
}
func (c *RealHTTPClient) Do(req *http.Request) (*http.Response, error) {
return c.client.Do(req)
}
測試時使用 httptest.Server:
func TestService_FetchRemote(t *testing.T) {
// 建立假伺服器
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"id":123,"status":"ok"}`)
}))
defer ts.Close()
// 使用內建 http.Client 指向測試伺服器
client := &RealHTTPClient{client: &http.Client{}}
svc := NewRemoteService(client, ts.URL)
resp, err := svc.GetStatus(context.Background())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.ID != 123 {
t.Errorf("expected ID 123, got %d", resp.ID)
}
}
範例 4️⃣ Mock 時間(time.Now)
時間相關的程式碼常常導致測試不穩定。將 time.Now 抽象成介面即可:
type Clock interface {
Now() time.Time
}
type RealClock struct{}
func (RealClock) Now() time.Time { return time.Now() }
測試時:
type FixedClock struct{ fixed time.Time }
func (c FixedClock) Now() time.Time { return c.fixed }
func TestService_Expiry(t *testing.T) {
fixed := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
svc := NewTokenService(FixedClock{fixed})
if !svc.IsExpired(time.Hour) {
t.Errorf("expected token to be expired")
}
}
範例 5️⃣ Mock 資料庫交易(Transaction)
如果程式使用 *sql.Tx,可以透過介面抽象交易行為,讓測試不必真的提交或回滾。
type Tx interface {
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
Commit() error
Rollback() error
}
在測試中使用 github.com/DATA-DOG/go-sqlmock:
func TestService_Transfer(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("failed to open sqlmock: %v", err)
}
defer db.Close()
// 設定期望的 SQL 執行順序
mock.ExpectBegin()
mock.ExpectExec("UPDATE accounts SET balance").
WithArgs(100, 1).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec("UPDATE accounts SET balance").
WithArgs(-100, 2).
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectCommit()
repo := NewSQLUserRepository(db)
svc := NewTransferService(repo)
if err := svc.Transfer(context.Background(), 1, 2, 100); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("unmet expectations: %v", err)
}
}
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 忘記介面化依賴 | 直接在程式碼裡 sql.DB、http.Client,導致無法注入 mock。 |
先設計介面,再在 main 或 DI 容器中注入真實實作。 |
| Mock 行為過於具體 | 把所有細節都寫進 mock,測試變成「實作驗證」而非「行為驗證」。 | 只模擬必要的輸入/輸出,使用 gomock.Any() 或 gomock.Match 來寬鬆匹配。 |
| 測試相依外部環境 | 測試需要真實資料庫或網路,CI 執行不穩定。 | 完全使用 mock 或在 CI 中使用 Docker 容器提供臨時環境。 |
| 未檢查 mock 呼叫次數 | 忽略 Times(),可能隱藏重複呼叫或遺漏呼叫的問題。 |
明確設定 Times(1)、Times(0),讓測試失敗時能快速定位。 |
| Mock 產生工具版本不一致 | 不同開發者使用不同版本的 mockgen,產生的程式碼差異大。 |
在 go.mod 中鎖定工具版本,或使用 go install 指定版本。 |
最佳實踐
- 介面只抽象行為,不要把資料結構也搬進介面。
- 測試命名:
Test<Service>_<Scenario>_<Expectation>,讓失敗訊息一目了然。 - 使用 table‑driven 測試,同一個測試函式跑多組資料,減少重複程式碼。
- 保持 mock 輕量:若需要大量行為,考慮拆分介面或使用 stub(回傳固定值)代替 full mock。
- 在 CI 中加入 race detector:
go test -race ./...,確保 mock 沒有引入競爭條件。
實際應用場景
微服務間的 RPC 呼叫
- 介面化 gRPC client,使用
gomock模擬回傳結果,驗證服務在不同錯誤碼下的容錯邏輯。
- 介面化 gRPC client,使用
資料庫遷移腳本
- 使用
sqlmock驗證 migration 程式在不同 schema 狀態下的 SQL 執行順序與錯誤處理。
- 使用
第三方支付 API
- 透過
httptest.Server建立假支付平台,測試簽名、回傳格式與重試機制。
- 透過
背景工作(worker)與佇列
- 把 Kafka/Redis client 抽象為介面,mock 消費者的
Consume方法,驗證工作者在訊息失敗時的重試與死信處理。
- 把 Kafka/Redis client 抽象為介面,mock 消費者的
功能旗標(Feature Flag)服務
- 使用 mock 的 flag provider,測試不同旗標組合下的業務分支,確保旗標開關不會破壞主流程。
總結
- Mock 是整合測試的核心工具,讓我們在不依賴外部資源的情況下,驗證系統的行為與邊界。
- 先 介面化依賴,再選擇 手寫或自動產生 的 mock;兩者皆有適用情境。
- 注意 呼叫次數、參數匹配,以及 避免過度耦合,才能寫出可維護、易閱讀的測試。
- 透過上述範例與最佳實踐,你可以在日常開發、CI/CD 流程中,快速建立可靠的整合測試,提升程式碼品質與部署信心。
祝你在 Go 的測試之路上,寫得更快、測得更穩! 🚀