testability-obstacle
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseResolve 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.
引入测试特定C#行为所需的最小行为保留接缝,然后添加既能验证行为又能验证接缝的确定性测试。生产代码的修改是为了实现所需测试的手段,而非重构相邻代码的借口。
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
, then generate tests separately.
migrate-static-to-wrapper - The code is not C#/.NET.
- 依赖项已被注入或作为参数传递。使用通过现有接缝编写测试假对象。
code-testing-agent - 用户需要对整个仓库进行可测试性审计。使用。
detect-static-dependencies - 用户希望生成包装器但不修改调用站点/测试。使用。
generate-testability-wrappers - 用户要求进行大范围的机械迁移。使用,然后单独生成测试。
migrate-static-to-wrapper - 代码并非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
步骤1:验证障碍存在
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
步骤2:选择最小的安全接缝
Choose by dependency and repository constraints:
| Dependency | Preferred seam |
|---|---|
| Current time / timers | Inject |
| Filesystem | Existing repository file abstraction; otherwise the smallest interface or |
| HTTP | Existing typed |
| Randomness | Inject |
| Environment/console/process | Minimal interface containing only members used by the target |
The scoped 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 as an unconditional assignment to .
AsyncLocal<T>Dispose()nullConstructor 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:
- flow across (
await, notAsyncLocal<T>);[ThreadStatic] - return and restore the previous value, including nested scopes;
IDisposable - 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 wrapper:
IDelay| Ambient operation | Replacement |
|---|---|
| |
| |
| |
Test delayed behavior by starting the operation, proving it is incomplete,
advancing , then awaiting it. Never wait for wall-clock time.
FakeTimeProviderFor a nested ambient override, disposing the inner scope must restore the outer
value, not clear the slot. Capture the previous value per scope:
csharp
public static IDisposable OverrideClock(Func<DateTimeOffset> clock)
{
var previous = s_clock.Value;
s_clock.Value = clock;
return new Scope(() => s_clock.Value = previous);
}Add tests for both nesting and parallel async flows; parallel-only tests do not
catch the common "dispose sets null" bug.
根据依赖项和仓库约束选择:
| 依赖项 | 首选接缝 |
|---|---|
| 当前时间/计时器 | 注入 |
| 文件系统 | 仓库中已有的文件抽象;否则使用最小接口,或在已使用/接受的情况下使用 |
| HTTP | 已有的类型化 |
| 随机性 | 注入 |
| 环境/控制台/进程 | 仅包含目标所使用成员的最小接口 |
对于所有必须保留公共静态形态的静态API(时钟、文件系统访问、环境查询、标识生成和随机性),均适用作用域规则。作用域会捕获并恢复之前的值;绝不要将实现为无条件赋值为。
AsyncLocal<T>Dispose()null构造函数注入是实例类的默认方式。复用仓库的DI和命名约定,但不要仅为满足本工作流向类库添加DI容器。
对于静态类或无法更改的公共API,仅在构造函数/参数注入不可行时才使用作用域环境接缝。覆盖必须:
- 支持跨流动(使用
await,而非AsyncLocal<T>);[ThreadStatic] - 返回并恢复之前的值,包括嵌套作用域;
IDisposable - 默认使用真实生产依赖项;
- 避免使用会导致测试无法并行的全局可变假对象。
使用内置的支持假时间的重载,而非自行实现包装器:
IDelay| 环境操作 | 替代方案 |
|---|---|
| |
| |
| 当目标框架支持时,使用 |
通过以下方式测试延迟行为:启动操作,验证其未完成,推进,然后等待完成。绝不要等待真实时钟时间。
FakeTimeProvider对于嵌套环境覆盖,释放内部作用域必须恢复外部值,而非清空槽位。每个作用域都要捕获之前的值:
csharp
public static IDisposable OverrideClock(Func<DateTimeOffset> clock)
{
var previous = s_clock.Value;
s_clock.Value = clock;
return new Scope(() => s_clock.Value = previous);
}添加针对嵌套和并行异步流的测试;仅并行测试无法发现常见的“释放时设为null”错误。
Step 3: Preserve behavior and API shape
步骤3:保留行为和API形态
Keep the production change mechanical:
- Wrap only members used by the target behavior.
- Default implementations delegate directly to the original API.
- Preserve exceptions, path handling, time zone, and .
DateTime.Kind - Keep existing public signatures unless the user explicitly permits an API change.
- Do not move business logic into the wrapper or fix unrelated production bugs.
For time replacements:
- ->
DateTime.UtcNowtimeProvider.GetUtcNow().UtcDateTime - ->
DateTime.NowtimeProvider.GetLocalNow().LocalDateTime - ->
DateTimeOffset.UtcNowtimeProvider.GetUtcNow() - ->
DateTimeOffset.NowtimeProvider.GetLocalNow()
保持生产代码变更的机械性:
- 仅包装目标行为所使用的成员。
- 默认实现直接委托给原始API。
- 保留异常、路径处理、时区和。
DateTime.Kind - 除非用户明确允许API变更,否则保持现有公共签名不变。
- 不要将业务逻辑移入包装器,也不要修复无关的生产代码bug。
对于时间替换:
- ->
DateTime.UtcNowtimeProvider.GetUtcNow().UtcDateTime - ->
DateTime.NowtimeProvider.GetLocalNow().LocalDateTime - ->
DateTimeOffset.UtcNowtimeProvider.GetUtcNow() - ->
DateTimeOffset.NowtimeProvider.GetLocalNow()
Step 4: Keep production defaults wired
步骤4:保持生产默认配置
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.
更新所有受接缝影响的组合根或构造函数调用。生产环境默认仍需使用真实时间/文件系统等。如果项目使用DI,按照仓库约定的生命周期注册默认实现。如果不使用DI,则显式组合;不要引入容器。
编写测试前,先构建受影响的生产项目。此处的编译失败属于接缝问题,而非测试问题。
Step 5: Write deterministic tests
步骤5:编写确定性测试
Use the repository's existing test project. If none exists, invoke
first.
scaffold-dotnet-test-projectTests must supply controlled dependencies:
- fixed/advanced time rather than wall-clock waiting;
- an in-memory fake filesystem or hand-rolled fake rather than temp/real files;
- no environment mutation, external process, console input, or network.
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.
使用仓库现有的测试项目。如果没有,先调用。
scaffold-dotnet-test-project测试必须提供受控依赖项:
- 使用固定/推进的时间,而非等待真实时钟;
- 使用内存中的假文件系统或手动实现的假对象,而非临时/真实文件;
- 不修改环境、不调用外部进程、不使用控制台输入或网络。
断言所需的业务结果,以及至少一个能证明假对象驱动执行路径的交互/状态可观察项。仅当生产默认测试能保持确定性时才添加;绝不要仅仅为了验证适配器的委托逻辑而访问真实文件系统。
Step 6: Verify the complete path
步骤6:验证完整路径
Run the affected production build, targeted test project, and repository-level
test command. Re-read the diff and confirm:
- every production change is required by the seam;
- no real ambient resource is used by the new tests;
- current-time semantics and public behavior are preserved;
- existing tests were not replaced or duplicated.
运行受影响的生产构建、目标测试项目和仓库级测试命令。重新查看差异并确认:
- 所有生产代码变更都是接缝所需的;
- 新测试未使用任何真实环境资源;
- 当前时间语义和公共行为得以保留;
- 现有测试未被替换或重复。
Output Contract
输出约定
Provide a compact 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.
Requirement | Evidence提供简洁的「需求 | 证据」表格。引用生产接缝、生产默认配置、确切测试名称和通过的命令。如果包还原或构建阻碍了验证,请报告该障碍,而非声称测试通过。
Validation
验证清单
- The original obstacle was concrete and in the requested path.
- An existing seam was reused when available.
- The new abstraction exposes only members required by the target behavior.
- Production defaults still delegate to the original dependency.
- Time conversions preserve local/UTC and semantics.
DateTime.Kind - Static ambient overrides are async-safe, scoped, nested, and reversible.
- New tests use fixed/in-memory dependencies and no real I/O or wall clock.
- Production build and targeted/repository tests pass.
- 原始障碍是具体的且位于请求路径中。
- 已有接缝在可用时被复用。
- 新抽象仅暴露目标行为所需的成员。
- 生产默认实现仍委托给原始依赖项。
- 时间转换保留了本地/UTC和语义。
DateTime.Kind - 静态环境覆盖支持异步安全、作用域、嵌套和可逆。
- 新测试使用固定/内存依赖项,无真实I/O或时钟等待。
- 生产构建和目标/仓库测试通过。
Common Pitfalls
常见陷阱
| 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 | Use |
| Mutable static fake shared by tests | Use constructor injection or a scoped |
| 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 |
| 陷阱 | 纠正措施 |
|---|---|
| 在验证障碍前就进行重构 | 复用已有接缝并直接编写测试 |
| 包装整个静态API | 仅暴露目标所使用的成员 |
使用 | 使用 |
| 测试间共享可变静态假对象 | 使用构造函数注入或作用域 |
| 向无容器的库添加DI | 显式组合依赖项 |
| 使用临时文件作为捷径 | 提供内存中的假对象;该场景无需真实I/O |
| 重构完成并构建后就停止 | 编写并运行证明接缝合理性的行为测试 |