generate-testability-wrappers
DO NOT USE when the target already consumes an injected interface or built-in abstraction such as IFileSystem or TimeProvider, even if the request says "generate a wrapper"; no new wrapper is needed.
Install / Use
npx skills add dotnet/skills --skill generate-testability-wrappersInstalls into whichever agent you are using.
SKILL.md
Installable skill definition
Quality Score
Category
Development & EngineeringSupported Platforms
Our assessment of generate-testability-wrappers
generate-testability-wrappers scores 87/100 on our quality scale, 887th of 2,398 Development & Engineering skills we index (top 37%).
Its SKILL.md is 15 KB long, well organised into 19 sections with 12 code examples: a thorough specification that gives an agent plenty to work with.
With 5,471 GitHub stars, it is one of the more widely adopted skills in the catalogue.
Maintenance, license and trust
- The repository was last updated 2 days ago, so generate-testability-wrappers is actively maintained.
- It is released under the MIT license, a permissive license that allows use, modification and commercial use with attribution.
- Its trust signals score 100/100, with no cautions. These come from repository metadata, not a code audit — read the skill file before letting an agent act on it.
generate-testability-wrappers compared with similar skills
All 4 of these similar skills score higher than generate-testability-wrappers; compare them before choosing.
| Skill | Score | Stars | Updated | Format |
|---|---|---|---|---|
| generate-testability-wrappers (this skill)by dotnet | 87 | 5.5k | 2d ago | SKILL.md |
| Agent-Reachby Panniantong | 100 | 85.6k | 11d ago | CLAUDE.md |
| headroomby headroomlabs-ai | 100 | 73.9k | today | CLAUDE.md |
| ai-job-searchby MadsLorentzen | 100 | 44.0k | 5d ago | CLAUDE.md |
| claude-howtoby luongnv89 | 100 | 41.7k | today | CLAUDE.md |
Frequently asked questions
- How do I install generate-testability-wrappers?
- Run
npx skills add dotnet/skills --skill generate-testability-wrappers. The install tabs above show the steps for each supported agent. - Which AI agents does generate-testability-wrappers work with?
- It is written for Universal, as a SKILL.md file. Other agents that read the same format can often use it too.
- Is generate-testability-wrappers safe to use?
- It is MIT-licensed and scores 100/100 on trust signals. Skills are instructions an agent will follow, so read the file before installing it and do not approve commands you do not understand.
- Is generate-testability-wrappers still maintained?
- The repository was last updated 2 days ago, so generate-testability-wrappers is actively maintained.
Skill content
View source on GitHubname: generate-testability-wrappers description: > DO NOT USE when the target already consumes an injected interface or built-in abstraction such as IFileSystem or TimeProvider, even if the request says "generate a wrapper"; no new wrapper is needed. Use only when C# source calls an ambient/static dependency and no injectable seam exists: first-time TimeProvider, IHttpClientFactory, or System.IO.Abstractions adoption; minimal Environment/Console/Process wrappers; IProcessRunner; DI registration; or an ambient seam that preserves a static API. Exclude static detection (detect-static-dependencies), migration to an existing/registered abstraction (migrate-static-to-wrapper), one blocked behavior plus deterministic tests (testability-obstacle), and general interface design. license: MIT
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 a released static API needs an ambient seam because signatures cannot change
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
If the target already consumes an injected interface or built-in abstraction, stop: do not add a second wrapper, project, registration, or test around that seam.
A missing DI package does not by itself force an ambient seam. For an instantiable class, prefer constructor injection and compose it explicitly or show the requested registration. Use Step 5 when the API is static and its signatures must stay static, or when the user explicitly forbids caller construction/DI changes.
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) |
| Composition | No | Existing DI framework, explicit/manual construction, or immutable static API |
| 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:
- instantiable class: constructor injection, even if current callers compose the object manually;
- existing container: add compile-ready registration following its conventions;
- public static API/signatures that cannot change: Step 5's ambient seam.
Check for a host builder, IServiceCollection, existing registrations, and
construction sites. Do not infer "must remain static" merely because the project
currently has no container.
Step 2: Generate built-in abstraction adoption (Time, HTTP)
TimeProvider (.NET 8+)
No wrapper code needed. Complete all four parts: production registration,
constructor injection, a FakeTimeProvider test, and the testing package.
- 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));
The assertion must prove a time-dependent result after the fake is pinned or
advanced. Merely constructing FakeTimeProvider is not a test. When the project
has no container but the target is an instantiable class, inject
TimeProvider anyway and show explicit production construction with
TimeProvider.System; do not replace it with a custom static clock.
Before calling the adoption complete, verify the repository contains or the answer supplies every required artifact: the testing package reference, the production composition/registration, every affected constructor call, and a runnable fake-time test. A code fragment that omits one of those integration points is guidance, not a completed adoption.
TimeProvider (pre-.NET 8)
Guide: install Microsoft.Bcl.TimeProvider NuGet. Same API as above.
IHttpClientFactory
No wrapper code needed. Register a typed client via
builder.Services.AddHttpClient<MyService>() and inject HttpClient directly
into the class constructor. Preserve cancellation by passing the caller's token
to the HTTP operation.
For tests, provide a complete fake HttpMessageHandler whose SendAsync
returns a deterministic HttpResponseMessage, construct HttpClient with that
handler, and exercise the typed client without network access. Compile and run
the focused test when the task asks for implementation; do not stop at a
schematic handler method.
When production uses a typed-client registration, test that same registration
pipeline: configure its primary handler in ServiceCollection, resolve the typed
client, and call it. A test that manually constructs HttpClient proves the class
but not the DI registration the task asked to adopt.
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.
Prefer a stateless operation-shaped interface. For example, if a caller only
needs to start a process, wait, and return its exit code, expose one Run
operation rather than a stateful wrapper that leaks Process lifecycle.
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>>();
Treat registration as a deliverable, not a sentence in the summary. Add it to the repository's existing registration surface when one exists. Otherwise show the exact compile-ready statement and identify where the caller should place it. Stateless delegating wrappers are singleton; if state must be retained, explain why a shorter lifetime is required.
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"));
Package-first adoption is exclusive: add both package references, use
IFileSystem in production, register or explicitly compose FileSystem, and
seed MockFileSystem before exercising the consumer. Do not also generate a
second custom filesystem interface, and do not present an unseeded mock whose
test could pass without proving the requested read/write behavior.
Step 5: Generate a signature-preserving ambient context
Use this pattern when the API must remain static or its released signatures cannot accept a dependency:
public static class Clock
{
private static readonly AsyncLocal<Func<DateTime>?> s_override = new();
public static DateTime UtcNow
=> s_override.Value?.Invoke() ?? TimeProvider.System.GetUtcNow().UtcDateTime;
internal static IDisposable Override(DateTime fixedUtcTime)
{
if (fixedUtcTime.Kind != DateTimeKind.Utc)
throw new ArgumentException("The override must be UTC.", nameof(fixedUtcTime));
var previous = s_override.Value;
s_override.Value = () => fixedUtcTime;
return new Scope(previous);
}
private sealed class Scope : IDisposable
{
private readonly Func<DateTime>? _previous;
private bool _disposed;
public Scope(Func<DateTime>? previous)
{
_previous = previous;
}
public void Dispose()
{
if (_disposed)
return;
s_override.Value = _previous;
_disposed = true;
}
}
}
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(). - Prove the test assembly can reach the override. An
internaloverride is inaccessible from a separate test assembly unless the production project adds the exactInternalsVisibleTofor that test assembly. Otherwise use an already-public seam only when a public API change is authorized. Never show a test calling an inaccessible member.
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 semanti
Truncated for display — read the full file on GitHub.
Related Skills
Agent-Reach
85.6kGive your AI agent eyes to see the entire internet. Read & search Twitter, Reddit, YouTube, GitHub, Bilibili, XiaoHongShu — one CLI, zero API fees.
headroom
73.9kCompress tool outputs, logs, files, and RAG chunks before they reach the LLM. 20% fewer tokens for coding agents, 60-95% fewer tokens for JSON, same answers. Library, proxy, MCP server.
ai-job-search
44.0kThe job search that runs on your machine. AI job application framework built on Claude Code: evaluate postings, tailor CVs, write cover letters, prep interviews. Fork it and own it.
claude-howto
41.7kA visual, example-driven guide to Claude Code — from basic concepts to advanced agents, with copy-paste templates that bring immediate value.
Languages
Trust signals
From repository metadata: license, adoption, age and documentation. Not a code audit — see the Safety scan above for what the skill file itself contains.
