generate-testability-wrappers
Skill9 starsChanged 6 months agoMITView on GitHub
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.
1 other project uses a version of this, 1 of them edited.
Adopted by
Most recently taken up by
- microsoft/testfxedited, first seen Oct 9, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 1
- microsoft/testfx★ 1.1k37% same
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository29 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- cli-e2e-testingSkill
- crap-scoreSkill
- detect-static-dependenciesSkill
- dotnet-livecharts2Skill
- dotnet-modern-csharp-editorconfigSkill
- dotnet-test-frameworksSkill
- filter-syntaxSkill
- generate-testability-wrappersSkill
- migrate-static-to-wrapperSkill
- mtp-hot-reloadSkill
- platform-detectionSkill
- run-testsSkill
- test-anti-patternsSkill
- writing-mstest-testsSkill
- cli-e2e-testingSkill
- crap-scoreSkill
- detect-static-dependenciesSkill
- dotnet-livecharts2Skill
- dotnet-modern-csharp-editorconfigSkill
- dotnet-test-frameworksSkill
- filter-syntaxSkill
- migrate-static-to-wrapperSkill
- mtp-hot-reloadSkill
- platform-detectionSkill
- run-testsSkill
- test-anti-patternsSkill
- writing-mstest-testsSkill
The file
We split this skill into the rules it gives an agent
11 rules. About 0 are common, 4 are distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 16
After running detect-static-dependencies and identifying which statics to wrap
- Distinctive: line 17
When the user asks to make a class testable by replacing statics with injected abstractions
- Very specific: line 18
When adopting TimeProvider (.NET 8+) or System.IO.Abstractions
- Very specific: line 19
When creating a custom wrapper for Environment.*, Console.*, or Process.*
- Distinctive: line 56
No wrapper code needed — guide the user:
- Very specific: line 87
No wrapper code needed — register typed clients via builder.Services.AddHttpClient<MyService>() and inject HttpClient directly into the …
- Distinctive: line 95
Only include methods that were actually detected in the codebase.
- Very specific: line 134
Prefer the established System.IO.Abstractions NuGet package over custom wrappers:
- Distinctive: line 170
If the codebase does not use DI (e.g., old console app, library code), offer the ambient context pattern:
- Very specific: line 196
If there is an Abstractions/ or Interfaces/ folder, place the interface there
- Very specific: line 197
If there is an Infrastructure/ or Services/ folder, place the implementation there
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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.
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 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: 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 |
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.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)
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.
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)
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>, scoped disposal, and trade-off explanation
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 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> |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

