Best for
- A requested test would otherwise read/write the real filesystem.
- Behavior depends on the current time, delay, random value, environment, console,
- The user explicitly permits or requests a safe production seam.
dotnet/skills/plugins/dotnet-test/skills/testability-obstacle/SKILL.md
C#/.NET test generation that requires the smallest production seam for DateTime/Task.Delay/File/Environment/Guid/Random, static API preservation, nested/parallel overrides, or no real I/O. USE ONLY when the target workspace contains C# source plus a .csproj or .sln. DO NOT USE for audits, bulk migration, code that already has an injectable seam, or an explicit migration to a user-named existing abstraction (migrate-static-to-wrapper). Use instead of general test generation when the requested tes
Decision brief
Introduce the smallest behavior-preserving seam needed to test a specific C behavior, then add deterministic tests that prove both the behavior and the seam. The production edit is a means to the requested test, not an invitation to redesign adjacent code.
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/testability-obstacle"Inspect the Agent Skill "testability-obstacle" from https://github.com/dotnet/skills/blob/2b9056bd9152490cc698c5b3e61c9f9a1c135776/plugins/dotnet-test/skills/testability-obstacle/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
Read the target production path and its existing tests. Identify the exact ambient operation preventing a deterministic test and the behavior that must remain unchanged. Do not run a repository-wide static scan for a single-class request.
Read the target production path and its existing tests. Identify the exact ambient operation preventing a deterministic test and the behavior that must remain unchanged. Do not run a repository-wide static scan for a single-class request.
Choose by dependency and repository constraints:
Keep the production change mechanical:
Update every composition root or constructor call affected by the seam. Production must still use real time/filesystem/etc. by default. If the project uses DI, register the default implementation with the lifetime matching repository conventions. If it does not use DI, compose e…
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
Introduce the smallest behavior-preserving seam needed to test a specific C# behavior, then add deterministic tests that prove both the behavior and the seam. The production edit is a means to the requested test, not an invitation to redesign adjacent code.
code-testing-agent.detect-static-dependencies.generate-testability-wrappers.migrate-static-to-wrapper, then generate tests separately.TimeProvider or
IFileSystem and asks to migrate call sites to it. Use
migrate-static-to-wrapper, which also updates affected tests.| Input | Required | Description |
|---|---|---|
| Behavior to test | Yes | The method/workflow and expected observable behavior |
| Target scope | No | Discover the narrowest relevant file/project when omitted |
| Allowed production changes | No | Default to the minimum internal/constructor seam |
Read the target production path and its existing tests. Identify the exact ambient operation preventing a deterministic test and the behavior that must remain unchanged. Do not run a repository-wide static scan for a single-class request.
If an adequate seam already exists, stop refactoring and use it. This skill adds no value when a fake can already be supplied.
Choose by dependency and repository constraints:
| Dependency | Preferred seam |
|---|---|
| Current time / timers | Inject TimeProvider; use FakeTimeProvider in tests |
| Filesystem | Existing repository abstraction; for one write/read operation use an injected delegate when conventions allow, otherwise a one-member interface or an already accepted System.IO.Abstractions |
| HTTP | Existing typed HttpClient/handler or IHttpClientFactory seam |
| Randomness | One generated value: injected delegate with Random.Shared as the production default; multiple operations/state: inject Random or a minimal generator interface |
| Environment/console/process | Minimal interface containing only members used by the target |
The scoped AsyncLocal<T> rule applies to every static API that must retain its
public static shape — clocks, filesystem access, environment lookups, identity
generation, and randomness. The scope captures and restores the previous value;
never implement Dispose() as an unconditional assignment to null.
Store the provider/value itself in AsyncLocal<T>. Do not put a mutable
Stack<T>, list, or other shared mutable collection in the slot: child
execution contexts can inherit the same object and corrupt each other's nesting.
When the provider itself is mutable (for example an in-memory store or fake time
provider), establish a fresh provider inside each parallel flow rather than
mutating one inherited instance from a parent context.
Constructor injection is the default for instance classes. Reuse the repository's DI and naming conventions, but do not add a DI container to a class library just to satisfy this workflow.
For a static class or a public API that cannot change, use a scoped ambient seam only when constructor/parameter injection is impossible. The override must:
await (AsyncLocal<T>, not [ThreadStatic]);IDisposable and restore the previous value, including nested scopes;Use built-in fake-time-aware overloads instead of inventing an IDelay wrapper:
| Ambient operation | Replacement |
|---|---|
Task.Delay(delay, token) | Task.Delay(delay, timeProvider, token) |
new CancellationTokenSource(delay) | new CancellationTokenSource(delay, timeProvider) |
PeriodicTimer(period) | new PeriodicTimer(period, timeProvider) when the target framework provides it |
Test delayed behavior by starting the operation, proving it is incomplete,
advancing FakeTimeProvider, then awaiting it. For a deadline or boundary,
advance to immediately before the deadline and assert the task is still
incomplete before advancing across it; an immediate post-start assertion alone
does not prove the boundary. Never wait for wall-clock time.
For a nested ambient override, each scope owns the value that was active when it
started. Dispose scopes in LIFO order with using (which emits try/finally) or
an explicit finally; disposing the inner scope restores the outer value, never
an unconditional null. For an environment-backed static API, use this shape:
public static class FeatureFlags
{
private static readonly AsyncLocal<Func<string, string?>?> s_environment = new();
public static bool IsEnabled(string name)
{
var reader = s_environment.Value;
var value = reader is null
? Environment.GetEnvironmentVariable(name)
: reader(name);
return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
}
public static IDisposable OverrideEnvironment(Func<string, string?> reader)
{
ArgumentNullException.ThrowIfNull(reader);
var previous = s_environment.Value;
s_environment.Value = reader;
return new RestoreScope(() => s_environment.Value = previous);
}
private sealed class RestoreScope : IDisposable
{
private Action? _restore;
public RestoreScope(Action restore)
{
_restore = restore;
}
public void Dispose() =>
Interlocked.Exchange(ref _restore, null)?.Invoke();
}
}
The exception test must observe the outer value after the exception has escaped
the inner using scope but before the outer scope is disposed:
using var outer = FeatureFlags.OverrideEnvironment(_ => "true");
Assert.True(FeatureFlags.IsEnabled("Preview"));
Assert.Throws<InvalidOperationException>(() =>
{
using var inner = FeatureFlags.OverrideEnvironment(_ => "false");
Assert.False(FeatureFlags.IsEnabled("Preview"));
throw new InvalidOperationException("test");
});
Assert.True(FeatureFlags.IsEnabled("Preview"));
Also overlap two async flows that each establish a fresh override and assert that each flow sees only its own value. Parallel-only tests do not catch the common "dispose sets null" bug. Do not mutate process environment variables in these tests; the scoped reader is the deterministic input. Choose an outer value different from the production fallback so clearing the slot cannot accidentally pass the restoration assertion.
Keep the production change mechanical:
DateTime.Kind.Deterministic serialized text is a deliberate exception to preserving ambient
platform formatting. If the user asks for exact reproducible output across
platforms, use the format's explicit separator (use literal \n when none is
specified) and assert that literal content. Keep Environment.NewLine only
when platform-native output is part of the existing contract.
For time replacements:
DateTime.UtcNow -> timeProvider.GetUtcNow().UtcDateTimeDateTime.Now -> timeProvider.GetLocalNow().LocalDateTimeDateTimeOffset.UtcNow -> timeProvider.GetUtcNow()DateTimeOffset.Now -> timeProvider.GetLocalNow()Update every composition root or constructor call affected by the seam. Production must still use real time/filesystem/etc. by default. If the project uses DI, register the default implementation with the lifetime matching repository conventions. If it does not use DI, compose explicitly; do not introduce a container.
Build the affected production project before writing tests. A compile failure here is a seam problem, not a test problem.
Use the repository's existing test project. If none exists, invoke
scaffold-dotnet-test-project first.
Tests must supply controlled dependencies:
Assert the requested business result and at least one interaction/state observable that proves the fake dependency drove the path. Include a production-default test only when it can remain deterministic; never touch the real filesystem merely to prove the adapter delegates.
Choose the narrowest seam that supports the behavior. A single
File.WriteAllText call can be an injected Action<string, string> with a real
default; do not create an interface, implementation, friend-assembly setting,
and extra project wiring unless repository conventions or multiple operations
justify them.
Do not add InternalsVisibleTo merely to reach a constructor-injected delegate
or other seam that the test project can already supply. Friend-assembly access
is justified only when the chosen minimum seam must remain internal and the
exact test assembly is known.
Run the affected production build, targeted test project, and repository-level test command. Re-read the diff and confirm:
Inspect the test summary, not only the exit code. Zero discovered tests, a build without the requested test run, or any failing/erroring test means the task is incomplete. Fix discovery/execution and rerun before reporting success. For a static ambient seam, completion requires executed tests for substitution, nested restoration, and overlapping async-flow isolation; production compilation alone is never sufficient. Capture the passing test count or requested test names in the handoff. If no test was discovered or the output does not prove execution, correct the project/test source and rerun rather than reporting the seam as validated.
Provide a compact Requirement | Evidence table. Cite the production seam,
production default wiring, exact test names, and passing commands. If a package
restore or build blocks validation, report that blocker rather than claiming the
tests pass.
DateTime.Kind semantics.| Pitfall | Corrective action |
|---|---|
| Refactoring before proving a blocker | Reuse an existing seam and write the test directly |
| Wrapping an entire static API | Expose only members exercised by the target |
Converting UtcNow with .DateTime | Use .UtcDateTime to preserve DateTimeKind.Utc |
| Mutable static fake shared by tests | Use constructor injection or a scoped AsyncLocal<T> override |
| Adding DI to a library with no container | Compose the dependency explicitly |
| Using temp files as a shortcut | Supply an in-memory fake; the scenario requires no real I/O |
| Stopping after the refactor builds | Write and run the behavior tests that justified the seam |
| Reporting a zero-test run as success | Fix discovery and require the requested tests to execute and pass |
Frequently asked questions
Introduce the smallest behavior-preserving seam needed to test a specific C behavior, then add deterministic tests that prove both the behavior and the seam. The production edit is a means to the requested test, not an invitation to redesign adjacent code.
The source record exposes this install command: npx skills add https://github.com/dotnet/skills --skill "plugins/dotnet-test/skills/testability-obstacle". 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
vipshop/cache-dit
High-level guide for integrating a new DiT model into cache-dit: Cache (BlockAdapter/ForwardPattern), Context Parallelism, Tensor Parallelism, Text Encoder Parallelism (TE-P), VAE Parallelism (VAE-P), generate CLI, installation, testing workflow, and detailed references. Use when adding support for a new diffusion transformer model in cache-dit.