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
managedcode-dotnet-skills
481182 skills17 agents
Install
$ npx -y skills add managedcode/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 withmanagedcode-dotnet-skills

Stop explaining .NET to your AI. Start building. We've all been there: asking Claude to use Entity Framework, only to get EF6 patterns in a .NET 8 project. Explaining to Copilot that Blazor Server and Blazor WebAssembly aren't the same thing.

Get the whole plugin

Other skills on managedcode-dotnet-skills.