Best for
- After running detect-static-dependencies and 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+) or System.IO.Abstractions
dotnet/skills/plugins/dotnet-test/skills/generate-testability-wrappers/SKILL.md
Generate C# testability abstractions and DI registration when none exists: minimal Environment/Console/Process wrappers, or first-time TimeProvider, IHttpClientFactory, and System.IO.Abstractions adoption. USE FOR: generate a wrapper for statics, make a class testable, wrap DateTime/File/Process, create IProcessRunner, add DI registration, or preserve a static API with an ambient seam. DO NOT USE FOR: wrapping an API already consumed through an interface or built-in abstraction such as IFileSyst
Decision brief
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 al…
Compatibility matrix
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/dotnet/skills --skill "plugins/dotnet-test/skills/generate-testability-wrappers"Inspect the Agent Skill "generate-testability-wrappers" from https://github.com/dotnet/skills/blob/2b9056bd9152490cc698c5b3e61c9f9a1c135776/plugins/dotnet-test/skills/generate-testability-wrappers/SKILL.md at commit 2b9056bd9152490cc698c5b3e61c9f9a1c135776. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
Based on the category and target framework:
Based on the category and target framework:
No wrapper code needed. Complete all four parts: production registration, constructor injection, a FakeTimeProvider test, and the testing package.
For categories without built-in abstractions, follow this template:
Review the “Default implementation — delegate to the real static” section in the pinned source before continuing.
Permission review
No configured static risk pattern was detected
This is not proof of safety. Runtime behavior, indirect dependencies, and hidden external systems are outside the static scan.
Evidence record
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 97/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 5,277 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
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)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.
| 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 |
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:
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.
No wrapper code needed. Complete all four parts: production registration,
constructor injection, a FakeTimeProvider test, and the testing package.
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));
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.
Guide: install Microsoft.Bcl.TimeProvider NuGet. Same API as above.
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.
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.
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>);
}
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>>();
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.
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"));
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.
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:
IDisposable that restores the previous value, so a test cannot leak a pinned time into the next one. A bare setter, or a manual try/finally at each call site, puts that burden on every test author.AsyncLocal<T>, never [ThreadStatic]. [ThreadStatic] does not flow across await, so the override silently disappears mid-test.DateTime.UtcNow with a local-time source changes the DateTimeKind every existing caller and stored value depends on — pair UtcNow with GetUtcNow(), and Now with GetLocalNow().internal override is
inaccessible from a separate test assembly unless the production project adds
the exact InternalsVisibleTo for 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 semantics.
Generate files following the project's existing conventions:
Abstractions/ or Interfaces/ folder, place the interface thereInfrastructure/ or Services/ folder, place the implementation thereAlways generate:
Skip registration 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.
Before reporting completion, verify the delivered output contains every item the prompt requested. In particular, do not summarize "singleton registration" when no registration code was added or shown, and do not claim testability without demonstrating how the consumer receives a fake.
AddSingleton for stateless wrappers, AddTransient for stateful onesTimeProvider is recommended over custom ISystemClockFakeTimeProvider, its testing package, and an assertion on a
time-dependent resultAsyncLocal<T>, a scoped IDisposable that restores the previous value, and trade-off explanationIServiceCollection registration is proposed, the separate test assembly can reach the override, and the replaced member's return type and semantics (UtcNow vs Now, and its DateTimeKind) are preserved| Pitfall | Solution |
|---|---|
| Treating "no DI package" as "must be ambient" | Inject into instantiable classes and compose explicitly; reserve Step 5 for static/signature-preserving APIs |
| 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> |
| Showing an internal ambient override to external tests | Add the exact friend assembly or use an authorized public seam; compile the test project |
Frequently asked questions
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 al…
The source record exposes this install command: npx skills add https://github.com/dotnet/skills --skill "plugins/dotnet-test/skills/generate-testability-wrappers". Inspect the command and pinned source before running it.
Alternatives
garrytan/gbrain
End-to-end discipline for turning any large data source (audio libraries, email takeouts, document corpora, chat exports, API dumps) into brain pages at scale. The lifecycle spine: SCHEMA → ACCESS → TRIAL → EVALUATE → IMPROVE → CODIFY → TEST → SKILLIFY → BULK → MONITOR. State is tracked in a durable JSON manifest (see MANIFEST-PATTERN.md) so any crash, session boundary, or subagent fan-out resumes from ground truth instead of memory.
alirezarezvani/claude-skills
App Store Optimization (ASO) toolkit for researching keywords, analyzing competitor rankings, generating metadata suggestions, and improving app visibility on Apple App Store and Google Play Store. Use when the user asks about ASO, app store rankings, app metadata, app titles and descriptions, app store listings, app visibility, or mobile app marketing on iOS or Android. Supports keyword research and scoring, competitor keyword analysis, metadata optimization, A/B test planning, launch checklist
dotnet/skills
Migrates .NET test projects from VSTest to Microsoft.Testing.Platform (MTP). Use when user asks to "migrate to MTP", "switch from VSTest", "enable Microsoft.Testing.Platform", "use MTP runner", set OutputType=Exe only for test projects in Directory.Build.props, or mentions EnableMSTestRunner, EnableNUnitRunner, or UseMicrosoftTestingPlatformRunner. USE FOR: MTP behavioral differences vs VSTest (exit code 8, zero tests discovered, --ignore-exit-code, TESTINGPLATFORM_EXITCODE_IGNORE); centralizing
oaustegard/claude-skills
Generate hierarchical _FEATURES.md files that describe what a codebase DOES from a user/consumer perspective, anchored to source symbols via tree-sitting. Supports large complex codebases through feature-driven decomposition into sub-feature files. Uses a multi-pass synthesis: orientation → detail → overview rewrite. Use when someone says "what does this do", "document features", "feature inventory", "_FEATURES.md", or needs to understand a codebase's purpose before modifying it. Complements tre