/testability-obstacle
MUST USE for C#/.NET deterministic tests that require 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
Claude Code
$ npx -y skills add dotnet/skills --skill testability-obstacle --agent claude-code
Codex
$ npx skills add dotnet/skills --skill testability-obstacle -a codex -y
Cursor
$ npx skills add dotnet/skills --skill testability-obstacle -a cursor -y
Gemini CLI
$ npx skills add dotnet/skills --skill testability-obstacle -a gemini-cli -y
GitHub Copilot
$ npx skills add dotnet/skills --skill testability-obstacle -a github-copilot -y
OpenCode
$ npx skills add dotnet/skills --skill testability-obstacle -a opencode -y
Windsurf
$ npx skills add dotnet/skills --skill testability-obstacle -a windsurf -y
SKILL.md
testability-obstacle.SKILL.mdname: testability-obstacle
description: >-
MUST USE for C#/.NET deterministic tests that require 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 test is impossible without a
production edit and seam selection is still open.
license: MIT
Resolve a Testability Obstacle
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.
When to Use
- A requested test would otherwise read/write the real filesystem.
- Behavior depends on the current time, delay, random value, environment, console,
process, or another ambient dependency.
- The user explicitly permits or requests a safe production seam.
- Existing tests cannot control a dependency without process-global mutation.
When Not to Use
- The dependency is already injected or passed as an argument. Write tests with
a fake through the existing seam using `code-testing`.
- The user wants a repository-wide testability audit. Use
`detect-static-dependencies`.
- The user wants wrappers generated but not call sites/tests changed. Use
`generate-testability-wrappers`.
- The user requests a broad mechanical migration. Use
`migrate-static-to-wrapper`, then generate tests separately.
- The user already selected an existing replacement such as `TimeProvider` or
`IFileSystem` and asks to migrate call sites to it. Use `migrate-static-to-wrapper`, which also updates affected tests.
- The code is not C#/.NET.
Inputs
| 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 |
Workflow
Step 1: Prove the obstacle
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.
Step 2: Select the smallest safe seam
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 final generated value: inject `Func<int>` and keep range selection in the real default; inject `Func<int, int, int>` only when range arguments are behavior the test must verify; 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.
Preserve the existing public construction surface unless the user authorizes an API change. Keep a public parameterless constructor as the real-dependency default and place a test-only delegate/provider constructor at the narrowest visibility the test project can reach. Do not turn the seam into a new public optional parameter merely for test convenience.
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:
- flow across `await` (`AsyncLocal<T>`, not `[ThreadStatic]`);
- return `IDisposable` and restore the previous value, including nested scopes;
- default to the real production dependency;
- avoid a process-global mutable fake that makes tests non-parallel.
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 dead
Read more
name: testability-obstacle description: >- MUST USE for C#/.NET deterministic tests that require 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 test is impossible without a production edit and seam selection is still open. license: MIT
Resolve a Testability Obstacle
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.
When to Use
- A requested test would otherwise read/write the real filesystem.
- Behavior depends on the current time, delay, random value, environment, console,
process, or another ambient dependency.
- The user explicitly permits or requests a safe production seam.
- Existing tests cannot control a dependency without process-global mutation.
When Not to Use
- The dependency is already injected or passed as an argument. Write tests with
a fake through the existing seam using `code-testing`.
- The user wants a repository-wide testability audit. Use
`detect-static-dependencies`.
- The user wants wrappers generated but not call sites/tests changed. Use
`generate-testability-wrappers`.
- The user requests a broad mechanical migration. Use
`migrate-static-to-wrapper`, then generate tests separately.
- The user already selected an existing replacement such as `TimeProvider` or
`IFileSystem` and asks to migrate call sites to it. Use `migrate-static-to-wrapper`, which also updates affected tests.
- The code is not C#/.NET.
Inputs
| 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 |
Workflow
Step 1: Prove the obstacle
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.
Step 2: Select the smallest safe seam
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 final generated value: inject `Func<int>` and keep range selection in the real default; inject `Func<int, int, int>` only when range arguments are behavior the test must verify; 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.
Preserve the existing public construction surface unless the user authorizes an API change. Keep a public parameterless constructor as the real-dependency default and place a test-only delegate/provider constructor at the narrowest visibility the test project can reach. Do not turn the seam into a new public optional parameter merely for test convenience.
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:
- flow across `await` (`AsyncLocal<T>`, not `[ThreadStatic]`);
- return `IDisposable` and restore the previous value, including nested scopes;
- default to the real production dependency;
- avoid a process-global mutable fake that makes tests non-parallel.
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 dead
This repository contains the .NET team's curated set of portable skills and host-specific custom agents for coding agents. For information about the Agent Skills standard, see agentskills.io.
Other skills on skills.
More skills from other plugins.
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
ask-matt
Ask which skill or flow fits your situation. A router over the skills in this repo.
Questions about testability-obstacle.
- Which coding agents can use testability-obstacle?
- testability-obstacle installs in Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode and Windsurf with the skills CLI, using each agent's flag shown on this page. Flowy's auto-invocation, which fires a skill when your prompt matches, runs in Claude Code only.

