Golang单元测试中如何模拟外部依赖

Go单元测试应通过接口抽象和依赖注入隔离外部依赖,用手工mock、httptest.Server或内存SQLite替代硬编码调用,避免gomock和httpmock等易失效方案。

用接口抽象 + 依赖注入替代硬编码调用

Go 的单元测试无法直接“打桩”第三方 HTTP 客户端或数据库驱动,核心解法是提前把外部依赖抽象成接口,并通过构造函数或方法参数注入。这样测试时就能传入 mock 实现,彻底隔离真实服务。

常见错误是直接在业务逻辑里写 http.DefaultClient.Do()sql.Open(),导致测试必须连网或启数据库。正确做法是定义接口并让结构体持有一个该接口字段:

type PaymentService interface {
    Charge(amount float64, cardToken string) error
}

type OrderProcessor struct {
    payment PaymentService // 依赖接口,而非具体实现
}

测试时只需提供一个满足该接口的 fake 结构体,无需任何第三方库。

手动实现 mock 接口比用 gomock 更轻量且可控

gomock 生成代码冗长、难调试,且容易因接口变更导致编译失败。多数场景下,手写 mock 更快、更直观,也更容易覆盖边界逻辑(如模拟超时、空响应、特定错误)。

立即学习“go语言免费学习笔记(深入)”;

例如模拟一个失败的支付服务:

type failingPaymentService struct{}

func (f *failingPaymentService) Charge(amount float64, cardToken string) error {
    return fmt.Errorf("payment declined: %s", cardToken)
}

使用时直接传入:processor := &OrderProcessor{payment: &failingPaymentService{}}。这种写法清晰暴露了行为契约,也方便在测试中组合不同返回路径。

  • 不要为每个方法都写完整 mock —— 只实现当前测试用到的方法即可
  • mock 方法内部避免调用真实网络或磁盘,否则就不是单元测试了
  • 若需验证调用次数或参数,可在 mock 中加字段记录,比如 calledWithAmount float64

HTTP 依赖优先用 httptest.Server 而非 httpmock

当业务代码依赖 http.Client 调用外部 API 时,最可靠的方式是启动一个本地 httptest.Server,让它返回预设响应。它完全走真实 HTTP 栈,能捕获 client 配置问题(如 timeout、header 设置),而 httpmock 这类纯拦截方案会绕过 Transport 层,掩盖配置缺陷。

示例:

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]string{"status": "success"})
}))
defer srv.Close()

client := &http.Client{Timeout: time.Second}
resp, _ := client.Get(srv.URL + "/pay") // 真实发起请求

注意:srv.URL 是可访问地址,srv.Close() 必须调用,否则端口泄漏。

数据库依赖用内存 SQLite 或 sqlmock(谨慎)

真实 PostgreSQL/MySQL 启动成本高、状态难清理;纯内存 SQLite(sqlite3://file::memory:?cache=shared)适合多数 CRUD 场景,支持事务、外键,且每个 test case 可重建 schema。

sqlmock 虽能断言 SQL 语句,但容易让测试过度耦合实现细节(比如“必须调用 WHERE id = ?”),一旦重构 SQL 就要改测试。更健壮的做法是:用内存 DB 执行真实查询,再校验结果是否符合预期。

关键点:

  • 所有 DB 初始化(db.Exec("CREATE TABLE..."))放在 TestXxx 函数开头,不复用连接
  • 避免在 init() 或包级变量中打开 DB,否则并发测试会冲突
  • 如果必须验证 SQL,确保只断言必要部分(如表名、WHERE 字段),忽略排序、括号格式等无关差异

真实依赖越少、抽象越早、mock 越简单,测试才越稳定。很多人卡在“不知道该 mock 哪一层”,其实答案很直接:只要不是当前包定义的类型,就该被替换。