microsoft/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. 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. sites (use migrate-static-to-wrapper), general interface design not about testability.
npx skills add https://github.com/microsoft/testfx --skill 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.
detect-static-dependencies and identifying which statics to wrapTimeProvider (.NET 8+) or System.IO.AbstractionsEnvironment.*, Console.*, or Process.*detect-static-dependencies)migrate-static-to-wrapper)| 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 |
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 |
No wrapper code needed — guide the user:
builder.Services.AddSingleton(TimeProvider.System);
public class OrderProcessor(TimeProvider timeProvider)
{
public bool IsExpired(Order order)
=> timeProvider.GetUtcNow() > order.ExpiresAt;
}
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));
Guide: install Microsoft.Bcl.TimeProvider NuGet. Same API as above.
No wrapper code needed — register typed clients via builder.Services.AddHttpClient<MyService>() and inject HttpClient directly into the class constructor.
For categories without built-in abstractions, follow this template:
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>);
}
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>);
}
// In Program.cs or Startup.cs:
builder.Services.AddSingleton<I<WrapperName>, <WrapperName>>();
Prefer the established System.IO.Abstractions NuGet package over custom wrappers:
dotnet add package System.IO.Abstractions
builder.Services.AddSingleton<IFileSystem, FileSystem>();
IFileSystem into classes:public class ConfigLoader(IFileSystem fileSystem)
{
public string LoadConfig(string path)
=> fileSystem.File.ReadAllText(path);
}
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"));
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.
Generate files following the project's existing conventions:
Abstractions/ or Interfaces/ folder, place the interface thereInfrastructure/ or Services/ folder, place the implementation thereAlways generate:
AddSingleton for stateless wrappers, AddTransient for stateful onesTimeProvider is recommended over custom ISystemClockAsyncLocal<T>, scoped disposal, and trade-off explanation| 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 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> |
Take microsoft/generate-testability-wrappers from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.