
Generate Testability Wrappers
FreeEasily create testable wrappers for static dependencies in C#.
Free · Opens the source repo
What Generate Testability Wrappers does
Generate Testability Wrappers is designed for developers who need to make static dependencies in their C# applications more testable. Often, static classes and methods can complicate unit testing, as they introduce tight coupling and make it difficult to isolate components. This skill provides a systematic approach to creating wrapper interfaces and dependency injection (DI) registration for these hard-to-test static dependencies. By generating abstractions for commonly used static classes like File, Console, and Process, developers can replace static calls with injected dependencies, thereby improving testability and maintainability of their code.
The skill is particularly useful when working with .NET projects that do not yet have a DI container in place. It offers a way to implement an ambient context seam, allowing developers to inject dependencies without requiring a full DI framework. This means that even if you're working on legacy code or a new project that hasn't adopted DI yet, you can still benefit from the testability improvements this skill provides. The generated wrappers help in creating a clean separation between your application logic and static calls, facilitating easier unit testing and mocking.
Use this skill after identifying static dependencies in your codebase. It guides you through the process of wrapping those dependencies, whether they are built-in abstractions available in .NET or custom implementations you need to create. The skill also assists in adopting features like TimeProvider and IHttpClientFactory, ensuring that your application remains up-to-date with best practices in dependency management and testability. Whether you are a seasoned developer or just starting with unit testing, this skill provides a clear path to making your codebase more robust and testable.
When to use it
Use this skill when you have identified static dependencies that need to be wrapped for testability or when adopting built-in abstractions like `TimeProvider` or `IHttpClientFactory`.
When not to use it
Avoid using this skill if you need to detect static dependencies or bulk-replace existing static calls, as those tasks require different tools.
What you can build with it
Wrapping File System Calls
Use this skill to create an `IFileSystem` wrapper for file operations, allowing for easier unit testing.
Adopting TimeProvider
Implement `TimeProvider` in your application to manage time-dependent logic and facilitate testing with a mock time provider.
Creating Custom Wrappers for Console
Generate a custom `IConsole` wrapper to replace static console calls, making it easier to test console output.
How to install Generate Testability Wrappers
View source1. Install with the skills CLI
npx skills add dotnet/skills/generate-testability-wrappers --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by dotnetGenerate 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.
When to Use
- After running
detect-static-dependenciesand 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+) orSystem.IO.Abstractions - When creating a custom wrapper for
Environment.*,Console.*, orProcess.* - When there is no DI container and the seam has to be ambient rather than injected
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
A project with no DI container, or a user who does not want to add one, is not a reason to skip this skill — that is exactly what the ambient context seam in Step 5 is for. Choose the seam over constructor injection in that case; do not decline the request and do not propose registering anything in a service collection.
Inputs
| Input | Required | Description |
|---|---|---|
| Static category | Yes | Which category: time, filesystem, environment, network, console, process |
| Target framework | Yes | The TargetFramework from .csproj (affects which built-in abstractions exist) |
| DI container | No | Which DI framework: microsoft (default), autofac, none (ambient context) |
| Namespace | No | Target namespace for generated wrapper code |
Workflow
Step 1: Determine the abstraction strategy
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) | Same | Same |
| HTTP | IHttpClientFactory (built-in) | Same | Same |
| Environment | Custom IEnvironmentProvider | Same | Same |
| Console | Custom IConsole | Same | Same |
| Process | Custom IProcessRunner | Same | Same |
The table picks which abstraction. How it reaches the code under test is a separate axis: constructor
injection when a DI container exists, and the ambient context seam of Step 5 when one does not. Decide that
axis first — check for a host builder, IServiceCollection, or an existing container registration — because a
static class cannot take a constructor and a project without a container has nowhere to register anything. In
that case skip Steps 2–4 and go to Step 5; the abstraction chosen above still applies, it is just reached through
the ambient seam.
Step 2: Generate built-in abstraction adoption (Time, HTTP)
TimeProvider (.NET 8+)
No wrapper code needed — guide the user:
- 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)
Guide: install Microsoft.Bcl.TimeProvider NuGet. Same API as above.
IHttpClientFactory
No wrapper code needed — register typed clients via builder.Services.AddHttpClient<MyService>() and inject HttpClient directly into the class constructor.
Step 3: Generate custom wrappers (Environment, Console, Process)
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.
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
Prefer the established System.IO.Abstractions NuGet package over custom wrappers:
- Install the package:
dotnet add package System.IO.Abstractions
- Register in DI:
builder.Services.AddSingleton<IFileSystem, FileSystem>();
- Inject
IFileSysteminto classes:
public class ConfigLoader(IFileSystem fileSystem)
{
public string LoadConfig(string path)
=> fileSystem.File.ReadAllText(path);
}
- Test with
MockFileSystem:
dotnet add <TestProject> package System.IO.Abstractions.TestingHelpers
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)
If the codebase does not use DI (e.g., old console app, library code), offer the ambient context pattern:
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.
Three properties this pattern must keep, because each has broken a real migration:
- Scope the override and make it reversible. Return an
IDisposablethat restores the previous value, so a test cannot leak a pinned time into the next one. A bare setter, or a manualtry/finallyat each call site, puts that burden on every test author. - Use
AsyncLocal<T>, never[ThreadStatic].[ThreadStatic]does not flow acrossawait, so the override silently disappears mid-test. - Preserve the semantics of the member you are replacing. Substituting
DateTime.UtcNowwith a local-time source changes theDateTimeKindevery existing caller and stored value depends on — pairUtcNowwithGetUtcNow(), andNowwithGetLocalNow().
The same shape works for non-time statics: swap TimeProvider.System.GetUtcNow() for the real static call and keep the override slot, the disposable scope, and the original semantics.
Step 6: Place generated files
Generate files following the project's existing conventions:
- If there is an
Abstractions/orInterfaces/folder, place the interface there - If there is an
Infrastructure/orServices/folder, place the implementation there - Otherwise, create files next to the code that uses the static
Always generate:
- 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) — skip this one entirely on the ambient-seam path: there is no container to register into, and offering one anyway is the failure mode that made a user ask for the seam in the first place
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
AddSingletonfor stateless wrappers,AddTransientfor stateful ones - NuGet packages are recommended where established libraries exist (System.IO.Abstractions, etc.)
- For .NET 8+,
TimeProvideris recommended over customISystemClock - Ambient context pattern includes
AsyncLocal<T>, a scopedIDisposablethat restores the previous value, and trade-off explanation - On the ambient-seam path, no
IServiceCollectionregistration is proposed and the replaced member's semantics (UtcNowvsNow, and itsDateTimeKind) are preserved
Common Pitfalls
| Pitfall | Solution |
|---|---|
| Declining because the project has no DI container | The ambient seam in Step 5 is the answer for that case — offer it instead of asking the user to adopt a container |
| Wrapping ALL members of a static class | Only wrap methods actually called in the codebase |
| Custom time wrapper on .NET 8+ | Use built-in TimeProvider instead |
| Custom file system wrapper | Prefer System.IO.Abstractions NuGet — battle-tested, complete |
| Registering scoped when singleton suffices | Stateless 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> |
Frequently asked questions about Generate Testability Wrappers
Similar skills
React Composition Patterns
Streamline your React component architecture with proven patterns.
Pester Should Migration
Easily convert Pester v5 assertions to v6 syntax.
Radix to Base UI Migration
Seamlessly migrate React components from Radix UI to Base UI.
Migrate Next.js to Vinext
Seamlessly transition your Next.js projects to Vinext.
WinUI 3 Migration Guide
Streamline your UWP to WinUI 3 migration process.
Refactor
Enhance code maintainability without altering behavior.
