generate-testability-wrappers
Generate wrapper interfaces and DI registration for hard-to-test static dependencies in C#. Produces IFileSystem, IEnvironmentProvider, IConsole, IProcessRunner wrappers, or guides adoption of TimeProvider and IHttpClientFactory. USE FOR: generate wrapper for static, create IFileSystem wrapper, wrap DateTime.Now, make static testable, make class testable, create abstraction for File.*, generate DI registration, TimeProvider adoption, IHttpClientFactory setup, testability wrapper, mock-friendly interface, mock time in tests, create the right abstraction to mock, how to mock DateTime, test code using File.ReadAllText, what abstraction for Environment, how to make statics injectable, adopt System.IO.Abstractions, make file calls testable. DO NOT USE FOR: detecting statics (use detect-static-dependencies), migrating call sites (use migrate-static-to-wrapper), general interface design not about testability.
NPX Install
npx skill4agent add dotnet/skills generate-testability-wrappersTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →Generate Testability Wrappers
TimeProviderIHttpClientFactoryWhen to Use
- After running and identifying which statics to wrap
detect-static-dependencies - When the user asks to make a class testable by replacing statics with injected abstractions
- When adopting (.NET 8+) or
TimeProviderSystem.IO.Abstractions - When creating a custom wrapper for ,
Environment.*, orConsole.*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
Inputs
| Input | Required | Description |
|---|---|---|
| Static category | Yes | Which category: |
| Target framework | Yes | The |
| DI container | No | Which DI framework: |
| Namespace | No | Target namespace for generated wrapper code |
Workflow
Step 1: Determine the abstraction strategy
| Category | .NET 8+ | .NET 6-7 | .NET Framework |
|---|---|---|---|
| Time | | | Custom |
| File system | | Same | Same |
| HTTP | | Same | Same |
| Environment | Custom | Same | Same |
| Console | Custom | Same | Same |
| Process | Custom | Same | Same |
Step 2: Generate built-in abstraction adoption (Time, HTTP)
TimeProvider (.NET 8+)
- Register in DI:
builder.Services.AddSingleton(TimeProvider.System);- Inject into classes:
public class OrderProcessor(TimeProvider timeProvider)
{
public bool IsExpired(Order order)
=> timeProvider.GetUtcNow() > order.ExpiresAt;
}- Test with :
FakeTimeProvider
// 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)
Microsoft.Bcl.TimeProviderIHttpClientFactory
builder.Services.AddHttpClient<MyService>()HttpClientStep 3: Generate custom wrappers (Environment, Console, Process)
Interface — define the minimal surface
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
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
// In Program.cs or Startup.cs:
builder.Services.AddSingleton<I<WrapperName>, <WrapperName>>();Step 4: Generate file system wrapper adoption
System.IO.Abstractions- Install the package:
dotnet add package System.IO.Abstractions- Register in DI:
builder.Services.AddSingleton<IFileSystem, FileSystem>();- Inject into classes:
IFileSystem
public class ConfigLoader(IFileSystem fileSystem)
{
public string LoadConfig(string path)
=> fileSystem.File.ReadAllText(path);
}- Test with :
MockFileSystem
dotnet add <TestProject> package System.IO.Abstractions.TestingHelpersvar 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)
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 readonlyStep 6: Place generated files
- If there is an or
Abstractions/folder, place the interface thereInterfaces/ - If there is an or
Infrastructure/folder, place the implementation thereServices/ - Otherwise, create files next to the code that uses the static
- The interface file (or adoption instructions for built-in abstractions)
- The default implementation file
- The DI registration snippet (as a code comment at the bottom of the implementation, or as separate instructions)
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 for stateless wrappers,
AddSingletonfor stateful onesAddTransient - NuGet packages are recommended where established libraries exist (System.IO.Abstractions, etc.)
- For .NET 8+, is recommended over custom
TimeProviderISystemClock - Ambient context pattern includes , scoped disposal, and trade-off explanation
AsyncLocal<T>
Common Pitfalls
| Pitfall | Solution |
|---|---|
| Wrapping ALL members of a static class | Only wrap methods actually called in the codebase |
| Custom time wrapper on .NET 8+ | Use built-in |
| Custom file system wrapper | Prefer |
| Registering scoped when singleton suffices | Stateless wrappers should be |
| Forgetting test helper packages | |
Ambient context without | Non-async |