要让定时器代码可测试,关键在于将定时行为抽象为接口并依赖注入,测试时可用模拟实现替换真实定时器;例如 go 中定义 timer 接口含 after 方法。

要让定时器代码可测试,关键不是直接使用 time.Sleep 或系统级定时器(如 Go 的 time.Ticker、C# 的 System.Threading.Timer),而是把定时行为抽象成接口,并通过依赖注入传入——这样测试时就能用模拟实现替换真实定时器。
定义定时器接口
先剥离具体实现,定义一个简洁、行为明确的接口。例如在 Go 中:
type Timer interface {
After(d time.Duration) 这个接口只暴露核心能力,不绑定具体类型(比如不返回 *time.Ticker)。它足够轻量,也便于 mock。
业务逻辑接收定时器作为参数
把定时器作为构造函数或方法参数传入,而不是在内部直接调用 time.Tick:
- 避免在结构体内部 new 出具体定时器实例
- 让调用方决定用真实定时器还是测试用的模拟实现
- 例如:
func NewWorker(t Timer) *Worker,而非func NewWorker() *Worker
编写可控制的测试模拟
测试时实现一个“可控定时器”,比如返回已关闭的 channel、立即触发的 channel,或由测试逻辑手动触发的 channel:
- 用
make(chan time.Time, 1)创建带缓冲的 channel,测试中主动发送时间点 - 实现
After返回立即触发的 channel:ch := make(chan time.Time, 1); ch - 对周期性任务,可用
chan+ goroutine 控制发送节奏,或干脆用单次触发模拟关键路径
真实运行时注入标准库定时器
生产环境仍用标准库,但包装一层适配器使其满足接口:
type stdTimer struct{}
<p>func (stdTimer) After(d time.Duration) </p><p>func (stdTimer) Tick(d time.Duration) </p>启动时传入 stdTimer{},零额外开销,无缝对接现有生态。











