在前幾天,我們完成了單元測試、pprof 優化,以及 SearchService
介面層。今天的目標是:
/search
呼叫到 service,再到假 ES」的 整合測試。Elasticsearch 很重,不可能每次 CI/CD 都啟一個 cluster。
這時候,我們會用 fake service 來模擬 ES 行為:
前一天我們有定義:
// search.go
type SearchService interface {
Search(ctx context.Context, query string) ([]SearchResult, error)
}
type SearchResult struct {
ID string
Title string
}
今天我們新增一個 fake 版本:
// fake_es.go
package search
import "context"
type FakeSearchService struct{}
func (f *FakeSearchService) Search(ctx context.Context, query string) ([]SearchResult, error) {
if query == "golang" {
return []SearchResult{
{ID: "1", Title: "Golang Official"},
{ID: "2", Title: "Go by Example"},
}, nil
}
return []SearchResult{}, nil
}
我們的 /search
handler 透過 DI(依賴注入)傳入 service:
// handler.go
type SearchHandler struct {
Service SearchService
}
func (h *SearchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
q := r.URL.Query().Get("q")
results, err := h.Service.Search(ctx, q)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(results)
}
接著,用 httptest
模擬 HTTP 請求,寫第一個整合測試:
// handler_test.go
func TestSearchHandler_E2E(t *testing.T) {
fake := &search.FakeSearchService{}
h := &SearchHandler{Service: fake}
req := httptest.NewRequest("GET", "/search?q=golang", nil)
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
resp := w.Result()
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
var got []search.SearchResult
if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
t.Fatalf("decode error: %v", err)
}
if len(got) != 2 {
t.Errorf("expected 2 results, got %d", len(got))
}
}
這裡我們完全沒有啟動 ES,只是透過假 service,驗證 API → Service → Response 這條路徑是通的。
今天我們完成了 E2E 測試雛型:
httptest
驗證整條 API 流程。明天,我們就要真的打通 Elasticsearch,寫一個最小 smoke test,讓 /search
能對接本地 ES。