应将并发组件抽象为接口以提升可测试性。例如用TimerProvider替代time.Timer,用EventHandler接口模拟回调,避免直接调用time.After、time.Sleep或暴露裸chan;每个测试需独立状态,禁用全局变量和init启动goroutine;测试须覆盖超时、取消、关闭等边界,并用runtime.NumGoroutine()检测泄漏。
并发逻辑常依赖 time.Timer、sync.WaitGroup、chan 等底层设施,直接耦合会导致测试时无法控制超时、阻塞或事件节奏。应将这些行为抽象为接口,比如把定时触发逻辑封装成 TimerProvider 接口,让生产代码依赖接口,测试时注入可控制的模拟实现。
time.After() 或 time.Sleep() —— 它们不可 mock,且拖慢测试Send(msg interface{}) error),而非暴露裸 chan 类型context.Context 代替硬
编码的 time.Sleep(),测试中可立即取消全局变量(如包级 var mu sync.RWMutex 或 var cache map[string]int)会让测试相互污染。并发测试并行执行时,一个测试修改了全局缓存,可能让另一个测试失败,而且难以复现。
func NewService(opts ...Option) 构造,测试时传入内存版依赖init() 函数启动 goroutine —— 它在测试导入时就运行,无法拦截或重置testify/mock 或手工接口模拟替代真实 goroutine真正启动 goroutine 的代码(如 go worker.Do())在单元测试中不需实际并发,重点是验证调度逻辑、错误分支、状态流转是否正确。用模拟对象替代耗时或不可控的并发行为更高效。
type EventHandler interface { OnEvent(data Event) }),测试中实现空方法或断言调用次数sync.WaitGroup 的模拟版本(如返回固定值的 Add()/Done())绕过等待,快速验证路径覆盖go test -race 单独跑,但别把它混进常规单元测试 —— 那会掩盖设计缺陷并发代码里最易出错的是未响应 ctx.Done()、漏关 channel、或死等锁。测试不能只覆盖“正常流程”,必须主动构造超时、取消、关闭等边界。
立即学习“go语言免费学习笔记(深入)”;
context.WithTimeout(context.Background(), 10*time.Millisecond) 而非 context.Background(),确保能触发取消路径close(ch),验证接收方是否优雅退出(而非 panic 或 hang)runtime.NumGoroutine() 断言数量不变(注意基准值可能含测试框架协程,建议取差值)func TestWorker_ProcessWithCancel(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
w := NewWorker()
err := w.Process(ctx, "test")
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("expected DeadlineExceeded, got %v", err)
}
}
并发可测试性的核心不在“怎么写 goroutine”,而在“怎么让它不依赖 goroutine 的具体执行时机”。越早把时间、等待、调度这些非确定性因素抽离为可替换的契约,测试就越稳定、越快、越贴近真实问题。