generate-testability-wrappers

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Generate Testability Wrappers

生成可测试性包装类

Generate wrapper interfaces, default implementations, and DI service registration code for untestable static dependencies. For statics that already have .NET built-in abstractions (
TimeProvider
,
IHttpClientFactory
), guide adoption of the built-in. For statics without built-in alternatives, generate custom minimal wrappers.
为无法测试的静态依赖生成包装接口、默认实现以及DI服务注册代码。对于已有.NET内置抽象的静态类(如
TimeProvider
IHttpClientFactory
),指导用户采用内置方案;对于无内置替代方案的静态类,则生成自定义的极简包装类。

When to Use

适用场景

  • After running
    detect-static-dependencies
    and identifying which statics to wrap
  • When the user asks to make a class testable by replacing statics with injected abstractions
  • When adopting
    TimeProvider
    (.NET 8+) or
    System.IO.Abstractions
  • When creating a custom wrapper for
    Environment.*
    ,
    Console.*
    , or
    Process.*
  • 运行
    detect-static-dependencies
    工具并确定需要包装的静态类之后
  • 需要通过将静态类替换为可注入抽象来让某个类具备可测试性时
  • 采用
    TimeProvider
    (.NET 8+)或
    System.IO.Abstractions
  • Environment.*
    Console.*
    Process.*
    创建自定义包装类时

When Not to Use

不适用场景

  • The user wants to find statics first (use
    detect-static-dependencies
    )
  • The user wants to bulk-replace call sites (use
    migrate-static-to-wrapper
    )
  • The static is already behind an interface
  • The project does not use dependency injection and the user does not want to add it
  • 用户首先需要查找静态依赖(请使用
    detect-static-dependencies
    工具)
  • 用户需要批量替换调用站点(请使用
    migrate-static-to-wrapper
    工具)
  • 静态类已基于接口实现
  • 项目不使用依赖注入且用户无意添加依赖注入

Inputs

输入参数

InputRequiredDescription
Static categoryYesWhich category:
time
,
filesystem
,
environment
,
network
,
console
,
process
Target frameworkYesThe
TargetFramework
from
.csproj
(affects which built-in abstractions exist)
DI containerNoWhich DI framework:
microsoft
(default),
autofac
,
none
(ambient context)
NamespaceNoTarget namespace for generated wrapper code
输入项是否必填描述
静态类别类别选项:
time
filesystem
environment
network
console
process
目标框架来自
.csproj
文件的
TargetFramework
(影响可用的内置抽象)
DI容器DI框架选项:
microsoft
(默认)、
autofac
none
(环境上下文模式)
命名空间生成的包装代码的目标命名空间

Workflow

工作流程

Step 1: Determine the abstraction strategy

步骤1:确定抽象策略

Based on the category and target framework:
Category.NET 8+.NET 6-7.NET Framework
Time
TimeProvider
(built-in)
TimeProvider
via
Microsoft.Bcl.TimeProvider
NuGet
Custom
ISystemClock
File system
System.IO.Abstractions
(NuGet)
SameSame
HTTP
IHttpClientFactory
(built-in)
SameSame
EnvironmentCustom
IEnvironmentProvider
SameSame
ConsoleCustom
IConsole
SameSame
ProcessCustom
IProcessRunner
SameSame
根据类别和目标框架确定策略:
类别.NET 8+.NET 6-7.NET Framework
时间
TimeProvider
(内置)
通过
Microsoft.Bcl.TimeProvider
NuGet包使用
TimeProvider
自定义
ISystemClock
文件系统
System.IO.Abstractions
(NuGet包)
同上同上
HTTP
IHttpClientFactory
(内置)
同上同上
环境自定义
IEnvironmentProvider
同上同上
控制台自定义
IConsole
同上同上
进程自定义
IProcessRunner
同上同上

Step 2: Generate built-in abstraction adoption (Time, HTTP)

步骤2:生成内置抽象的采用方案(时间、HTTP)

TimeProvider (.NET 8+)

TimeProvider(.NET 8+)

No wrapper code needed — guide the user:
  1. Register in DI:
csharp
builder.Services.AddSingleton(TimeProvider.System);
  1. Inject into classes:
csharp
public class OrderProcessor(TimeProvider timeProvider)
{
    public bool IsExpired(Order order)
        => timeProvider.GetUtcNow() > order.ExpiresAt;
}
  1. Test with
    FakeTimeProvider
    :
csharp
// Requires Microsoft.Extensions.TimeProvider.Testing NuGet
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2026, 1, 15, 0, 0, 0, TimeSpan.Zero));
var processor = new OrderProcessor(fakeTime);
fakeTime.Advance(TimeSpan.FromDays(1));
Assert.True(processor.IsExpired(order));
无需生成包装代码,指导用户执行以下操作:
  1. 在DI中注册:
csharp
builder.Services.AddSingleton(TimeProvider.System);
  1. 注入到类中:
csharp
public class OrderProcessor(TimeProvider timeProvider)
{
    public bool IsExpired(Order order)
        => timeProvider.GetUtcNow() > order.ExpiresAt;
}
  1. 使用
    FakeTimeProvider
    进行测试:
csharp
// Requires Microsoft.Extensions.TimeProvider.Testing NuGet
var fakeTime = new FakeTimeProvider(new DateTimeOffset(2026, 1, 15, 0, 0, 0, TimeSpan.Zero));
var processor = new OrderProcessor(fakeTime);
fakeTime.Advance(TimeSpan.FromDays(1));
Assert.True(processor.IsExpired(order));

TimeProvider (pre-.NET 8)

TimeProvider(.NET 8之前版本)

Guide: install
Microsoft.Bcl.TimeProvider
NuGet. Same API as above.
指导:安装
Microsoft.Bcl.TimeProvider
NuGet包,API与上述一致。

IHttpClientFactory

IHttpClientFactory

No wrapper code needed — register typed clients via
builder.Services.AddHttpClient<MyService>()
and inject
HttpClient
directly into the class constructor.
无需生成包装代码,通过
builder.Services.AddHttpClient<MyService>()
注册类型化客户端,并将
HttpClient
直接注入到类的构造函数中。

Step 3: Generate custom wrappers (Environment, Console, Process)

步骤3:生成自定义包装类(环境、控制台、进程)

For categories without built-in abstractions, follow this template:
对于无内置抽象的类别,遵循以下模板:

Interface — define the minimal surface

接口——定义最小化接口面

Only include methods that were actually detected in the codebase. Do NOT generate a wrapper for every possible member — wrap only what is used.
csharp
namespace <Namespace>;

/// <summary>
/// Abstraction over <static class> for testability. 
/// </summary>
public interface I<WrapperName>
{
    // One method per detected static call
    <return type> <MethodName>(<parameters>);
}
仅包含代码库中实际调用的方法。不要为所有可能的成员生成包装类,仅包装实际使用的部分。
csharp
namespace <Namespace>;

/// <summary>
/// Abstraction over <static class> for testability. 
/// </summary>
public interface I<WrapperName>
{
    // One method per detected static call
    <return type> <MethodName>(<parameters>);
}

Default implementation — delegate to the real static

默认实现——委托给真实静态类

csharp
namespace <Namespace>;

/// <summary>
/// Default implementation that delegates to <static class>.
/// </summary>
public sealed class <WrapperName> : I<WrapperName>
{
    public <return type> <MethodName>(<parameters>)
        => <StaticClass>.<Method>(<arguments>);
}
csharp
namespace <Namespace>;

/// <summary>
/// Default implementation that delegates to <static class>.
/// </summary>
public sealed class <WrapperName> : I<WrapperName>
{
    public <return type> <MethodName>(<parameters>)
        => <StaticClass>.<Method>(<arguments>);
}

DI registration

DI注册

csharp
// In Program.cs or Startup.cs:
builder.Services.AddSingleton<I<WrapperName>, <WrapperName>>();
csharp
// In Program.cs or Startup.cs:
builder.Services.AddSingleton<I<WrapperName>, <WrapperName>>();

Step 4: Generate file system wrapper adoption

步骤4:生成文件系统包装类的采用方案

Prefer the established
System.IO.Abstractions
NuGet package over custom wrappers:
  1. Install the package:
dotnet add package System.IO.Abstractions
  1. Register in DI:
csharp
builder.Services.AddSingleton<IFileSystem, FileSystem>();
  1. Inject
    IFileSystem
    into classes:
csharp
public class ConfigLoader(IFileSystem fileSystem)
{
    public string LoadConfig(string path)
        => fileSystem.File.ReadAllText(path);
}
  1. Test with
    MockFileSystem
    :
dotnet add <TestProject> package System.IO.Abstractions.TestingHelpers
csharp
var mockFs = new MockFileSystem(new Dictionary<string, MockFileData>
{
    { "/config.json", new MockFileData("{\"key\": \"value\"}") }
});
var loader = new ConfigLoader(mockFs);
Assert.Equal("{\"key\": \"value\"}", loader.LoadConfig("/config.json"));
优先采用成熟的
System.IO.Abstractions
NuGet包,而非自定义包装类:
  1. 安装包:
dotnet add package System.IO.Abstractions
  1. 在DI中注册:
csharp
builder.Services.AddSingleton<IFileSystem, FileSystem>();
  1. IFileSystem
    注入到类中:
csharp
public class ConfigLoader(IFileSystem fileSystem)
{
    public string LoadConfig(string path)
        => fileSystem.File.ReadAllText(path);
}
  1. 使用
    MockFileSystem
    进行测试:
dotnet add <TestProject> package System.IO.Abstractions.TestingHelpers
csharp
var mockFs = new MockFileSystem(new Dictionary<string, MockFileData>
{
    { "/config.json", new MockFileData("{\"key\": \"value\"}") }
});
var loader = new ConfigLoader(mockFs);
Assert.Equal("{\"key\": \"value\"}", loader.LoadConfig("/config.json"));

Step 5: Generate ambient context alternative (when DI is not available)

步骤5:生成环境上下文替代方案(当无法使用DI时)

If the codebase does not use DI (e.g., old console app, library code), offer the ambient context pattern:
csharp
public static class Clock
{
    private static readonly AsyncLocal<Func<DateTimeOffset>?> s_override = new();
    public static DateTimeOffset UtcNow
        => s_override.Value?.Invoke() ?? TimeProvider.System.GetUtcNow();

    public static IDisposable Override(DateTimeOffset fixedTime)
    {
        s_override.Value = () => fixedTime;
        return new Scope();
    }
    private sealed class Scope : IDisposable
    {
        public void Dispose() => s_override.Value = null;
    }
}
Key trade-offs:
AsyncLocal<T>
ensures parallel tests don't interfere; production cost is one null check per call; the
static readonly
field is essentially free.
如果代码库不使用DI(例如旧控制台应用、类库代码),则提供环境上下文模式:
csharp
public static class Clock
{
    private static readonly AsyncLocal<Func<DateTimeOffset>?> s_override = new();
    public static DateTimeOffset UtcNow
        => s_override.Value?.Invoke() ?? TimeProvider.System.GetUtcNow();

    public static IDisposable Override(DateTimeOffset fixedTime)
    {
        s_override.Value = () => fixedTime;
        return new Scope();
    }
    private sealed class Scope : IDisposable
    {
        public void Dispose() => s_override.Value = null;
    }
}
关键权衡:
AsyncLocal<T>
确保并行测试不会互相干扰;生产环境的开销为每次调用一次空值检查;
static readonly
字段几乎无开销。

Step 6: Place generated files

步骤6:放置生成的文件

Generate files following the project's existing conventions:
  • If there is an
    Abstractions/
    or
    Interfaces/
    folder, place the interface there
  • If there is an
    Infrastructure/
    or
    Services/
    folder, place the implementation there
  • Otherwise, create files next to the code that uses the static
Always generate:
  1. The interface file (or adoption instructions for built-in abstractions)
  2. The default implementation file
  3. The DI registration snippet (as a code comment at the bottom of the implementation, or as separate instructions)
遵循项目现有约定放置生成的文件:
  • 如果存在
    Abstractions/
    Interfaces/
    文件夹,将接口文件放在其中
  • 如果存在
    Infrastructure/
    Services/
    文件夹,将实现文件放在其中
  • 否则,将文件放在使用该静态类的代码旁边
始终生成以下内容:
  1. 接口文件(或内置抽象的采用说明)
  2. 默认实现文件
  3. DI注册代码片段(可作为实现文件底部的代码注释,或单独的说明)

Validation

验证项

  • Generated interface only wraps statics that were actually detected (not speculative)
  • Default implementation delegates to the real static with no behavior changes
  • DI registration uses
    AddSingleton
    for stateless wrappers,
    AddTransient
    for stateful ones
  • NuGet packages are recommended where established libraries exist (System.IO.Abstractions, etc.)
  • For .NET 8+,
    TimeProvider
    is recommended over custom
    ISystemClock
  • Ambient context pattern includes
    AsyncLocal<T>
    , scoped disposal, and trade-off explanation
  • 生成的接口仅包装实际检测到的静态类(而非推测的)
  • 默认实现委托给真实静态类,无行为变更
  • DI注册中,无状态包装类使用
    AddSingleton
    ,有状态包装类使用
    AddTransient
  • 在已有成熟库的场景下推荐使用NuGet包(如System.IO.Abstractions等)
  • 对于.NET 8+,推荐使用
    TimeProvider
    而非自定义
    ISystemClock
  • 环境上下文模式包含
    AsyncLocal<T>
    、作用域释放以及权衡说明

Common Pitfalls

常见陷阱

PitfallSolution
Wrapping ALL members of a static classOnly wrap methods actually called in the codebase
Custom time wrapper on .NET 8+Use built-in
TimeProvider
instead
Custom file system wrapperPrefer
System.IO.Abstractions
NuGet — battle-tested, complete
Registering scoped when singleton sufficesStateless wrappers should be
AddSingleton
Forgetting test helper packages
Microsoft.Extensions.TimeProvider.Testing
for time,
System.IO.Abstractions.TestingHelpers
for filesystem
Ambient context without
AsyncLocal
Non-async
[ThreadStatic]
breaks with
async
/
await
— always use
AsyncLocal<T>
陷阱解决方案
包装静态类的所有成员仅包装代码库中实际调用的方法
在.NET 8+中使用自定义时间包装类改用内置的
TimeProvider
使用自定义文件系统包装类优先采用
System.IO.Abstractions
NuGet包——经过实战检验,功能完整
当单例足够时仍注册为作用域无状态包装类应使用
AddSingleton
忘记使用测试辅助包时间测试使用
Microsoft.Extensions.TimeProvider.Testing
,文件系统测试使用
System.IO.Abstractions.TestingHelpers
环境上下文未使用
AsyncLocal
非异步的
[ThreadStatic]
会在
async
/
await
场景下失效——始终使用
AsyncLocal<T>