Skip to content
Development
Skill

/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

From plugin
dotnet-skills
5.4k98 skills16 agents
Install
$ npx -y skills add dotnet/skills --skill testability-obstacle --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/testability-obstacle

Context preview

The summary Claude sees to decide when to auto-load this skill.

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

SKILL.md

testability-obstacle.SKILL.md
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-agent`.

  • 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 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.

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 deadline and assert the task is still incomplete before advancing across it; an immediate pos

Read more
Ships withdotnet-skills

This repository contains the .NET team's curated set of core skills and custom agents for coding agents. For information about the Agent Skills standard, see agentskills.io.

Get the whole plugin

Other skills on dotnet-skills.