后端与接口
当 C# 代码调用静态/环境依赖且没有可注入接缝时…
为不可测试的静态依赖生成包装接口、默认实现和 DI 注册,或引导采用 .NET 内置抽象(TimeProvider、IHttpClientFactory、System.IO.Abstractions)。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 49/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 85/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
当 C# 代码调用静态/环境依赖且没有可通用
解决思路
- 先判断抽象策略:根据类别和目标框架选择抽象方式——Time 在 .NET 8+ 用内置 TimeProvider,.NET 6-7 通过 Microsoft.Bcl.TimeProvider NuGet 使用 TimeProvider,.NET Framework 用自定义 ISystemClock;File system 用 System.IO.Abstractions NuGet;HTTP 用内置 IHttpClientFactory;Environment、Console、Process 用自定义 IEnvironmentProvider、IConsole、IProcessRunner。
- 确定抽象如何到达被测代码:可实例化的类用构造函数注入(即使当前调用方手动组合对象);已有容器则按其约定添加可编译的注册;公共静态 API 或签名不可更改时使用 Step 5 的环境接缝。检查 host builder、IServiceCollection、已有注册和构造点,不要仅因项目当前没有容器就推断“必须保持静态”。
- 采用 TimeProvider(.NET 8+):无需包装代码,完成四部分——在 DI 中注册 builder.Services.AddSingleton(TimeProvider.System);通过构造函数注入 TimeProvider 并在类中使用 timeProvider.GetUtcNow();用 FakeTimeProvider 编写测试(需 Microsoft.Extensions.TimeProvider.Testing NuGet),断言必须在固定或推进假时间后证明时间相关结果;仅构造 FakeTimeProvider 不算测试。项目无容器但目标可实例化时,仍注入 TimeProvider 并显式展示用 TimeProvider.System 的生产构造,不要替换为自定义静态时钟。完成前确认仓库或答案包含所有必需产物:测试包引用、生产组合/注册、所有受影响的构造函数调用、可运行的假时间测试。
- 采用 TimeProvider(.NET 8 之前):安装 Microsoft.Bcl.TimeProvider NuGet,API 同上。
- 采用 IHttpClientFactory:无需包装代码,通过 builder.Services.AddHttpClient<MyService>() 注册类型化客户端,直接将 HttpClient 注入类构造函数,保留取消操作(把调用方的 token 传给 HTTP 操作)。测试时提供完整的假 HttpMessageHandler,其 SendAsync 返回确定性的 HttpResponseMessage,用该 handler 构造 HttpClient,在无网络情况下测试类型化客户端;任务要求实现时编译并运行聚焦测试,不要停在示意性 handler 方法。生产使用类型化客户端注册时,测试同一注册管线:在 ServiceCollection 中配置其主 handler,解析类型化客户端并调用;手动构造 HttpClient 的测试只证明类,不证明任务要求采用的 DI 注册。
- 生成自定义包装器(Environment、Console、Process):接口只包含代码库中实际检测到的方法,不要为每个可能成员生成包装;优先无状态、面向操作的接口(例如只需启动进程、等待并返回退出码时,暴露一个 Run 操作而非泄漏 Process 生命周期的有状态包装)。默认实现委托给真实静态类,不改变行为。DI 注册作为交付物:在已有注册面添加,否则展示精确可编译语句并指明放置位置;无状态委托包装器用 AddSingleton,若需保留状态则解释为何需要更短生命周期。
- 采用文件系统包装(System.IO.Abstractions):安装 dotnet add package System.IO.Abstractions;在 DI 中注册 builder.Services.AddSingleton<IFileSystem, FileSystem>();将 IFileSystem 注入类中(如 fileSystem.File.ReadAllText(path));测试时安装 dotnet add <TestProject> package System.IO.Abstractions.TestingHelpers,用 MockFileSystem 预置数据后测试消费者。包优先采用是排他的:添加两个包引用、生产使用 IFileSystem、注册或显式组合 FileSystem、在测试消费者前预置 MockFileSystem;不要再生成第二个自定义文件系统接口,也不要展示未预置的 mock 使测试可能不证明所请求的读写行为。
- 生成保持签名的环境上下文(当 API 必须保持静态或已发布签名不能接受依赖时):使用 static readonly AsyncLocal<Func<DateTime>?> 覆盖槽,属性返回 s_override.Value?.Invoke() ?? TimeProvider.System.GetUtcNow().UtcDateTime;提供 internal static IDisposable Override(DateTime fixedUtcTime),校验 Kind 为 Utc 后保存前值并设置覆盖,返回的 Scope 在 Dispose 时恢复前值并防重复释放。必须保持三个属性:覆盖必须可作用域且可逆(返回 IDisposable 恢复前值,避免测试泄漏固定时间到下一个测试;裸 setter 或每个调用点手动 try/finally 会把负担推给每个测试作者);使用 AsyncLocal<T> 而非 [ThreadStatic](后者不跨 await 流动,覆盖会在测试中途静默消失);保留被替换成员的语义(UtcNow 配 GetUtcNow(),Now 配 GetLocalNow(),不要用本地时间源替换 DateTime.UtcNow 而改变 DateTimeKind)。证明测试程序集能访问覆盖:internal 覆盖需要生产项目添加精确的 InternalsVisibleTo 指向该测试程序集,否则仅在授权公共 API 变更时使用已公开接缝;绝不展示测试调用不可访问成员。同一形状适用于非时间静态:把 TimeProvider.System.GetUtcNow() 换成真实静态调用,保留覆盖槽、可释放作用域和原始语义。
- 放置生成文件:遵循项目现有约定——有 Abstractions/ 或 Interfaces/ 文件夹则放接口,有 Infrastructure/ 或 Services/ 文件夹则放实现,否则放在使用静态的代码旁边。始终生成:接口文件(或内置抽象的采用说明)、默认实现文件、可编译的 DI 注册(应用到已有注册面或展示在精确组合点)、确定性替换示例或聚焦测试(在不使用环境资源的情况下测试消费者)。环境接缝路径完全跳过注册:没有容器可注册,提供注册正是让用户要求接缝的失败模式。报告完成前验证交付输出包含提示要求的所有项:不要在没有添加或展示注册代码时总结“单例注册”,不要在没有演示消费者如何接收假对象时声称可测试。Console 包装器要用同时捕获提示并提供返回输入的假对象测试消费者;仅构建或只运行 banner 不证明提示流程。
- 按验证清单检查:生成的接口只包装实际检测到的静态(非推测);默认实现委托真实静态且无行为变化;无状态包装器用 AddSingleton,有状态用 AddTransient;存在成熟库时推荐 NuGet 包;.NET 8+ 推荐 TimeProvider 而非自定义 ISystemClock;TimeProvider 采用包含注入、生产组合、FakeTimeProvider、其测试包和对时间相关结果的断言;HTTP 采用包含完整的假 handler 测试并保留取消;注入路径上注册或显式组合可编译,假对象证明消费者无需真实环境依赖;环境上下文模式包含 AsyncLocal<T>、恢复前值的作用域 IDisposable 和权衡说明;环境接缝路径不提议 IServiceCollection 注册,独立测试程序集能访问覆盖,被替换成员的返回类型和语义(UtcNow vs Now 及其 DateTimeKind)保留。
适用边界 · 注意事项
- 目标已经消费注入接口或内置抽象(如 IFileSystem 或 TimeProvider)时不要使用,即使请求说“生成包装器”——不需要新包装器,不要在该接缝周围添加第二个包装器、项目、注册或测试。
- 用户想先查找静态依赖(用 detect-static-dependencies)、批量替换调用点(用 migrate-static-to-wrapper)、处理一个阻塞行为加确定性测试(用 testability-obstacle)或做通用接口设计时不要使用。
- 缺少 DI 包本身不强制使用环境接缝:对于可实例化的类,优先构造函数注入并显式组合或展示所请求的注册;仅当 API 是静态且签名必须保持静态,或用户明确禁止调用方构造/DI 变更时才用 Step 5。