最直接的错误模拟方式是用 errors.New 或 fmt.Errorf 创建可控错误,适用于简单判断场景;推荐通过接口替换实现 mock 注入,确保上下文错误使用标准值,并用 errors.Is/errors.As 断言。

用 errors.New 或 fmt.Errorf 构造可控错误
最直接的错误模拟方式是手动创建错误值,适用于被测函数内部不依赖具体错误类型、只做 err != nil 判断或简单字符串匹配的场景。注意不要在测试中用 errors.New("something went wrong") 这类模糊描述,而应使用有辨识度的字符串,方便断言定位。
- 若被测逻辑用
errors.Is判断底层错误,需确保注入的错误实现了嵌套(如用fmt.Errorf("wrap: %w", err)) - 若用
errors.As提取自定义错误类型,则必须传入对应类型的指针实例,不能只传errors.New - 避免在测试里重复定义错误变量;可统一放在
var块中,例如:var ( ErrNotFound = errors.New("not found") ErrTimeout = fmt.Errorf("timeout: %w", context.DeadlineExceeded) )
通过接口替换实现错误注入(推荐)
当被测函数依赖外部服务(如数据库、HTTP 客户端),应将依赖抽象为接口,并在测试时传入返回预设错误的 mock 实现。这是 Go 测试中最可靠、解耦最彻底的方式。
- 不要修改原函数签名强行加参数;而是提取依赖为字段或构造函数参数,例如:
type Service struct { db DBClient } func NewService(db DBClient) *Service { return &Service{db: db} } - mock 实现只需满足接口方法签名,错误返回写死即可:
type mockDB struct{} func (m mockDB) Query(ctx context.Context, sql string) (*Rows, error) { return nil, errors.New("simulated db failure") } - 注意上下文取消错误(
context.Canceled/context.DeadlineExceeded)需用标准值,否则errors.Is(err, context.Canceled)会失败
用函数字段替代硬编码调用
对无法轻易抽成接口的小型依赖(比如一个工具函数),可将其声明为结构体字段或包级变量,并在测试前替换为返回错误的闭包。
- 包级变量方式需配合
init或构造函数恢复默认值,防止测试污染:var timeNow = time.Now func TestSomething(t *testing.T) { defer func() { timeNow = time.Now }() timeNow = func() time.Time { return time.Unix(0, 0) } // ... } - 结构体字段更安全,但需确保被测逻辑确实通过该字段调用:
type Processor struct { nowFunc func() time.Time } func (p *Processor) Process() error { if p.nowFunc().After(deadline) { return errors.New("too late") } return nil } - 慎用
unsafe或反射篡改私有函数——这会让测试脆弱且难以维护
注意错误比较方式与测试断言粒度
Go 中错误不是值类型,直接用 == 比较仅适用于导出的包级错误变量或 errors.New 返回的同一实例。多数情况应使用 errors.Is 或 errors.As,否则测试容易误判。
- 如果被测函数返回的是
fmt.Errorf("failed: %w", originalErr),则断言必须用errors.Is(got, expected),而非got == expected - 避免只检查错误字符串包含某关键词(
strings.Contains(err.Error(), "timeout")),这属于脆弱断言;优先走类型或语义判断 - 对 HTTP handler 类测试,注意错误是否被中间件捕获并转为响应状态码——此时要检查响应体/状态码,而非原始 error 值
errors.Is,导致底层错误包装变化后测试无声失效。










