timeprovider 是整条时间调用链的控制开关,必须通过构造函数注入并统一传递至所有依赖节点(如 timer、task.delay),否则测试不可靠;testtimeprovider.advance() 仅累加偏移量,需主动调用驱动时间。

TimeProvider 不是用来替换 DateTime.UtcNow 的临时补丁,而是整条时间调用链的控制开关——漏掉任意一处隐式依赖(比如没传 TimeProvider 的 Timer 或 Task.Delay),测试就不可靠。
构造函数注入 TimeProvider 是唯一可靠起点
所有业务类必须把 TimeProvider 当作必填依赖传入,不能在方法体内硬编码 DateTime.UtcNow 或调用 TimeProvider.System。静态工厂方法(如 MyService.Create())容易绕过注入,需额外检查是否也接受 TimeProvider 参数。
- ✅ 正确:构造函数接收
TimeProvider并保存为readonly字段 - ❌ 错误:
public bool IsExpired(DateTimeOffset created) => DateTime.UtcNow > created + _validDuration; - ⚠️ 注意:
TimeProvider.System是单例、不可 mock,测试中必须显式传入TestTimeProvider
Timer 和 Task.Delay 必须显式传入 TimeProvider 才受控
.NET 8 中 Timer 和 Task.Delay 都有支持 TimeProvider 的重载,但不传就是走系统时钟。这是最常被忽略的点,哪怕其他逻辑全受控,只要这里漏了,测试就失真。
-
new Timer(callback, state, dueTime, period, timeProvider)—— 老签名(4 参数)完全不受控 -
Task.Delay(millisecondsDelay, cancellationToken, timeProvider)—— .NET 8 新增重载,旧版Task.Delay(int)无法模拟 - 第三方库(如 Hangfire、Quartz)若未暴露时钟抽象入口,其内部定时逻辑依然不可测
TestTimeProvider.Advance() 只累加偏移,不跳转绝对时间
TestTimeProvider.Advance() 模拟时间流逝,但它只是把内部偏移量累加,所有后续 GetUtcNow() 返回值 = 初始基准时间(通常是 Unix Epoch)+ 累计偏移。它不会“倒带”,也不会自动推进。
- 多次
Advance(1.Minutes())等价于一次Advance(2.Minutes()),顺序无关 - 如果测试中先调用
GetUtcNow()再Advance(),第一次拿到的是初始时间(不是当前系统时间) - 必须由测试代码主动调用
Advance()或SetUtcNow()来驱动时间,没有自动机制
从请求入口到最深异步延时,所有时间感知节点必须统一经过同一实例
真正难的不是写对一行 Advance(),而是确保从 HTTP 请求开始,到 Task.Delay、Timer 回调、超时判断、甚至日志时间戳,所有节点都使用同一个 TimeProvider 实例。任何一层绕过注入(比如某处用了 TimeProvider.System),都会让整个测试链失效。
尤其要注意中间件、过滤器、后台服务、以及封装了延时逻辑的工具类——它们往往隐藏着未显式传入 TimeProvider 的调用。










