C#
测试、调试与代码质量
通过 xUnit v3、Microsoft Testing Platform、分析器和调试工具建立快速质量反馈。
发布于 2026年7月23日
测试、调试与代码质量
质量工具的目标不是制造更多规则,而是缩短可信反馈周期:测试验证可观察行为,编译器与分析器验证类型和常见缺陷,格式化减少无意义差异,调试器与结构化日志帮助解释失败。
一、学习目标
- 使用 xUnit v3 编写行为测试
- 区分单元、集成和端到端测试
- 用可控时间、临时目录和替身隔离环境
- 测试异常、取消和事务回滚
- 启用编译器分析器与格式检查
- 使用调试器、堆栈和日志定位问题
二、建立测试项目
第 1 篇已经用固定版本的 v3 模板创建项目:
dotnet new install xunit.v3.templates::3.2.2
dotnet new xunit3 -n StudyTasks.Tests \
-o tests/StudyTasks.Tests -f net10.0
dotnet add tests/StudyTasks.Tests reference src/StudyTasks.Core
dotnet add tests/StudyTasks.Tests reference \
src/StudyTasks.Infrastructure
dotnet test StudyTasks.slnx -c Release
xUnit v3 测试项目是可执行程序,并通过 global.json 中的 "test": { "runner": "Microsoft.Testing.Platform" } 与 .NET 10 的 dotnet test 集成。项目依赖固定为 xunit.v3 3.2.2。
测试文件按被测行为命名,例如 StudyTaskTests.cs、SqliteTaskRepositoryTests.cs。不要建立一个包含数百个不相关用例的 Tests.cs。
三、第一个领域测试
public sealed class StudyTaskTests
{
[Fact]
public void Create_TrimsTitleAndCreatesPendingTask()
{
DateTimeOffset now =
new(2026, 7, 23, 12, 0, 0, TimeSpan.Zero);
StudyTask task = StudyTask.Create(
" 阅读 C# 文档 ",
now);
Assert.Equal("阅读 C# 文档", task.Title);
Assert.Equal(TaskStatus.Todo, task.Status);
Assert.Null(task.CompletedAt);
Assert.Equal(now, task.CreatedAt);
}
}
测试名表达“操作、条件、预期”。断言关注公开行为,不验证私有字段或具体辅助方法调用。
异常:
[Theory]
[InlineData("")]
[InlineData(" ")]
public void Create_RejectsBlankTitle(string title)
{
Assert.Throws<ArgumentException>(
() => StudyTask.Create(title, DateTimeOffset.UtcNow));
}
理论测试适合相同行为的多组输入,但失败信息不清楚时应拆成命名用例。
四、可控时间
internal sealed class FixedTimeProvider(
DateTimeOffset utcNow) : TimeProvider
{
public override DateTimeOffset GetUtcNow() => utcNow;
}
测试服务:
[Fact]
public void Complete_UsesInjectedUtcTime()
{
DateTimeOffset now =
new(2026, 7, 23, 13, 0, 0, TimeSpan.Zero);
var clock = new FixedTimeProvider(now);
var repository = new InMemoryTaskRepository();
StudyTask existing = repository.Add(
StudyTask.Create("测试任务", now.AddHours(-1)));
var service = new TaskService(repository, clock);
StudyTask completed = service.Complete(existing.Id);
Assert.Equal(now, completed.CompletedAt);
}
不要在测试里允许“当前时间前后几秒”的宽松断言,这会产生偶发失败并掩盖真实时区问题。
五、测试替身
常见替身:
- Stub:返回固定数据
- Fake:有可工作的简化实现,例如内存仓储
- Spy:记录调用以便断言
- Mock:按预期交互配置行为
优先使用简单手写 Fake:
internal sealed class StubRemoteTaskClient(
IReadOnlyList<StudyTask> tasks)
: IRemoteTaskClient
{
public Task<IReadOnlyList<StudyTask>> FetchAsync(
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return Task.FromResult(tasks);
}
}
只有交互本身是契约时才断言调用次数。过度 Mock 会把实现步骤写进测试,使安全重构也大量失败。
六、SQLite 集成测试
每个测试使用独立数据库。内存模式需要保持连接打开;更接近生产行为时使用临时目录文件。
public sealed class SqliteRepositoryTests : IDisposable
{
private readonly string _directory =
Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
public SqliteRepositoryTests()
{
Directory.CreateDirectory(_directory);
}
public void Dispose()
{
Directory.Delete(_directory, recursive: true);
}
}
覆盖:
- 新增返回数据库 ID
- 唯一与 CHECK 约束
- 状态和完成时间一起更新
- 未知 ID 不修改数据库
- 审计失败时事务回滚
- 连接释放后文件可删除
清理失败也应让测试失败,因为它可能暴露资源泄漏。
七、异步与取消测试
[Fact]
public async Task FetchAsync_PropagatesCancellation()
{
using var source = new CancellationTokenSource();
source.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(
() => client.FetchAsync(source.Token));
}
异步测试返回 Task,不要写 async void。使用已取消令牌、TaskCompletionSource 或可控处理器协调测试,不用长时间 Task.Delay 猜测调度顺序。
并发测试应证明不变量,例如最大同时请求数不超过配置,而不是依赖某一次日志顺序。
八、命令行测试
把命令构建与进程启动分开:
RootCommand command = CommandFactory.Create(service, output);
ParseResult result = command.Parse(["add", "阅读文档"]);
int exitCode = await result.InvokeAsync();
验证:
- 合法输入调用服务并输出稳定字段
- 非数字 ID 由解析器拒绝
- 未知 ID 使用 stderr 和退出码 3
--help不连接数据库- 日志不混入 stdout
少量真正的进程测试再验证打包后的可执行入口。
九、分析器和警告
Directory.Build.props:
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AnalysisLevel>latest-recommended</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
</Project>
构建:
dotnet build -c Release --warnaserror
dotnet format --verify-no-changes
抑制警告前先理解原因。确属误报时使用范围最小的抑制并写说明;不要在根配置中批量关闭未知规则。
升级 SDK 可能带来新分析规则。先单独评估,再决定修复、配置严重级别或有理由地抑制。
十、调试方法
先阅读:
- 异常类型与消息
- 最内层异常
- 第一处属于自己代码的堆栈帧
- 当前输入和相关结构化日志
调试器中使用条件断点、异常抛出时中断和线程/任务窗口。不要依赖修改生产代码加入大量 Console.WriteLine。
CLI 可用:
dotnet test --logger "console;verbosity=detailed"
dotnet test --filter "FullyQualifiedName~SqliteRepositoryTests"
复现问题后先补一个失败测试,再修复。测试应表达用户可观察行为,而不是把原缺陷实现复制一遍。
十一、常见错误
测试依赖真实时间和公网
结果会因环境变化。注入时间,HTTP 使用本地处理器。
只断言 Mock 调用
测试会与实现耦合。优先断言返回值和状态变化。
集成测试共享一个数据库
测试顺序会影响结果。每个测试独立数据和生命周期。
为了通过构建关闭警告
应修复根因或最小化、有依据地抑制。
十二、练习与自测
练习:
- 为
StudyTask的所有不变量补理论测试。 - 用 Fake 仓储测试服务,不断言私有调用。
- 写 SQLite 事务回滚集成测试。
- 写 CLI 解析、stdout/stderr 与退出码测试。
- 在一个测试中引入资源泄漏,让清理断言捕获它。
自测:
- Fake 与 Mock 的关注点有何不同?
- 为什么异步测试不能返回 void?
- 哪些行为必须用真实 SQLite 才能验证?
- 100% 覆盖率不能证明什么?
十三、官方资料
上一篇:异步、并发与取消 | 下一篇:NuGet、发布与部署