TimeProvider 是时间调用链的统一控制开关,必须全程显式注入并避免隐式依赖;TestTimeProvider.Advance() 累加偏移模拟流逝,Timer/Task.Delay 等需显式传入才受控,漏一处即导致测试失真。

TimeProvider 不是用来“手动替换 DateTime.Now”的补丁,而是整条时间调用链的控制开关——只要漏掉一处隐式依赖(比如没传入 TimeProvider 的 Timer 或 Task.Delay),测试就可能跑偏。
TimeProvider.System 是默认实现,但不能用于单元测试
它直接返回系统真实时间,无法 mock、无法推进、无法回拨。测试中若意外用了 TimeProvider.System,哪怕其他地方都用了 TestTimeProvider,也会因某处未注入而得到不可控结果。
- 构造函数注入是首选方式:把
TimeProvider当作必填依赖传入,避免在类内部硬编码调用DateTime.UtcNow或TimeProvider.System - 静态工厂方法或属性访问器(如
MyService.Create())容易绕过注入,需额外检查是否也接受TimeProvider参数 - .NET 8 中
Timer和Task.Delay都有支持TimeProvider的重载,但必须显式传入;不传就是走默认系统时钟
TestTimeProvider.Advance() 模拟时间流逝,但只影响后续调用
Advance(TimeSpan) 不会“倒带”或“跳转到绝对时间”,它只是把内部偏移量累加。所有后续 GetUtcNow() 返回值 = 初始基准时间 + 累计偏移。这意味着:
- 多次
Advance(1.Minutes())等价于一次Advance(2.Minutes()),顺序无关 - 如果测试中先调用
GetUtcNow()再Advance(),第一次拿到的是初始时间(通常是 Unix Epoch + 偏移 0),不是“当前系统时间” -
TestTimeProvider不自动推进,必须由测试代码主动调用Advance()或SetUtcNow()来驱动时间
Timer 和 Task.Delay 必须显式传入 TimeProvider 才受控
这是最容易被忽略的点。即使你的业务逻辑完全基于 TestTimeProvider,如果用了 new Timer(callback, state, dueTime, period) 这种老签名,它仍走系统时钟。
- 改用支持
TimeProvider的构造函数:new Timer(callback, state, dueTime, period, timeProvider) -
Task.Delay(millisecondsDelay, cancellationToken, timeProvider)是 .NET 8 新增重载,旧版Task.Delay(int)无法被模拟 - 第三方库(如 Hangfire、Quartz)若未适配
TimeProvider,其内部定时逻辑依然不可测——得看它是否暴露了时钟抽象入口
真正难的不是写对一行 Advance(),而是确保从入口请求开始,到最深的异步延时、定时回调、超时判断,所有时间感知节点都统一经过同一个 TimeProvider 实例。漏掉任意一环,测试就退化为“半真半假”。











