用途
该技能用于解决 C#/.NET 测试中的“可测试性障碍”:当请求编写的测试会读写真实文件系统,或行为依赖当前时间、延迟、随机值、环境、控制台、进程等环境依赖时,引入最小的、保持行为不变的接缝(seam),再补充确定性测试,同时验证业务行为和接缝本身。生产代码的改动只是手段,不应顺带重构相邻代码。
何时使用
- 待写测试会读写真实文件系统。
- 行为依赖当前时间、延迟、随机值、环境、控制台、进程或其它环境依赖。
- 用户明确允许或要求引入安全的生产接缝。
- 现有测试无法在不做进程级全局变更的情况下控制某依赖。
何时不使用
- 依赖已通过注入或参数传入:应直接用现有接缝配合假实现编写测试。
- 用户要做仓库级可测试性审计:应使用 detect-static-dependencies。
- 只需生成包装器而不改调用点/测试:应使用 generate-testability-wrappers。
- 用户要求大范围机械迁移:应先使用 migrate-static-to-wrapper,再单独生成测试。
- 用户已选定 TimeProvider、IFileSystem 等替代品并要求迁移调用点:应使用 migrate-static-to-wrapper。
- 代码不是 C#/.NET。
输入
- 待测行为(必填):方法与期望的可观察行为。
- 目标范围(可选):省略时自动发现最窄的相关文件/项目。
- 允许的生产改动(可选):默认为最小的内部/构造函数接缝。
工作流程
- 证明障碍:阅读目标生产路径及其现有测试,定位阻碍确定性测试的具体环境操作及必须保持不变的行为;不要为单个类请求做全仓库静态扫描;若已有足够接缝,停止重构直接使用。
- 选择最小安全接缝:按依赖类型选择——当前时间/计时器注入 TimeProvider(测试用 FakeTimeProvider);文件系统用已有抽象,单次读写可用注入委托,否则用单成员接口或已接受的 System.IO.Abstractions;HTTP 用已有类型化 HttpClient/handler 或 IHttpClientFactory 接缝;随机性对单个最终值注入 Func<int>(需验证范围参数时注入 Func<int, int, int>),多操作/状态则注入 Random 或最小生成器接口;环境/控制台/进程用仅含目标所用成员的最小接口。
- 保持行为与 API 形态:只包装目标行为用到的成员;默认实现直接委托原 API;保留异常、路径处理、时区和 DateTime.Kind;不改公开签名(用户明确允许除外);不把业务逻辑移入包装器,不修复无关缺陷。时间替换对应关系:DateTime.UtcNow → timeProvider.GetUtcNow().UtcDateTime;DateTime.Now → timeProvider.GetLocalNow().LocalDateTime;DateTimeOffset.UtcNow → timeProvider.GetUtcNow();DateTimeOffset.Now → timeProvider.GetLocalNow()。
- 保持生产默认接线:更新受影响的组合根或构造函数调用,生产默认仍用真实依赖;有 DI 时按仓库惯例注册默认实现,无 DI 时显式组合,不引入容器;先构建受影响的生产项目。
- 编写确定性测试:使用仓库现有测试项目(没有则先调用 scaffold-dotnet-test-project);用固定/推进时间而非等待真实时间,用内存假文件系统,不做环境变更、外部进程、控制台输入或网络;遵循既有测试框架与断言惯例,不添加手写 FactAttribute 或替代框架类型;断言业务结果及至少一个可观察状态证明假依赖驱动了路径。
- 验证完整路径:运行受影响的生产构建和最窄的目标测试命令;仅在用户要求广泛验证、仓库约定要求或接缝影响共享组合时运行仓库级测试;检查测试摘要而不仅是退出码,零发现测试或被阻塞的运行不算完成。
静态环境接缝的约束
对于必须保持公开静态形态的时钟、文件系统、环境查找、标识生成和随机性等静态 API,仅在构造函数/参数注入不可行时使用作用域环境接缝:必须跨 await 生效(用 AsyncLocal<T> 而非 [ThreadStatic]);返回 IDisposable 并恢复先前值(含嵌套作用域);默认指向真实生产依赖;避免进程级可变假实现导致测试无法并行。作用域捕获并恢复先前值,绝不能在 Dispose() 中无条件赋 null;槽位中存提供者/值本身,不要放可变 Stack<T>、列表等共享可变集合;提供者本身可变时,应在每个并行流内部新建提供者,而不是从父上下文变更继承的实例。嵌套覆盖应满足:每个作用域捕获启动时的值并恰好恢复一次,按 LIFO 顺序用 using/finally 释放;用不同值确保清空槽位不会误通过;并验证重叠的独立异步流各自只看到自己的覆盖。不要通过修改进程环境变量来测试环境接缝。延迟行为测试应:启动操作、证明未完成、推进 FakeTimeProvider、再等待完成;对截止/边界,先推进到截止前一刻断言仍未完成,再跨过边界验证。绝不要等待真实时间。可用的内置重载:Task.Delay(delay, token) → Task.Delay(delay, timeProvider, token);new CancellationTokenSource(delay) → new CancellationTokenSource(delay, timeProvider);PeriodicTimer(period) → new PeriodicTimer(period, timeProvider)(目标框架支持时)。
公开 API 与友元程序集
默认使用构造函数注入实例类,复用仓库 DI 与命名惯例,但不要仅为该流程给类库加 DI 容器;保留现有公开构造表面,保留公开无参构造函数作为真实依赖默认,把仅测试用的委托/提供者构造函数放到测试项目可访问的最窄可见性;不要为测试便利把接缝变成新的公开可选参数。仅当类当前只有隐式公开无参构造函数且确切测试程序集已知时,保留该构造函数行为并把测试专用构造函数设为 internal,此时 InternalsVisibleTo 是合理的;已有公开接缝可接受假实现时不要添加 InternalsVisibleTo。
输出契约
提供简洁的“需求 | 证据”表,引用生产接缝、生产默认接线、确切测试名称和通过的命令;若包还原或构建阻塞验证,应报告阻塞而非声称测试通过。
常见陷阱
在证明障碍前重构(应复用已有接缝直接写测试);包装整个静态 API(应只暴露目标用到的成员);用 .DateTime 转换 UtcNow(应用 .UtcDateTime 保留 DateTimeKind.Utc);测试共享可变静态假实现(应用构造函数注入或作用域 AsyncLocal<T> 覆盖);给无容器的库加 DI(应显式组合依赖);用临时文件走捷径(应用内存假实现);重构构建通过就停止(应编写并运行证明接缝必要的行为测试);把零测试运行报告为成功(应修复发现机制并确保请求的测试执行通过)。
适用场景与限制
适用于 C#/.NET 项目中因环境依赖而难以确定性测试的具体行为,以及用户明确许可引入最小生产接缝的场景。不适用于非 C#/.NET 代码、仓库级审计、仅生成包装器或大范围迁移等需求(应改用相应技能)。资料未提供该技能的版本、安装命令、收费或平台支持信息。