1. 课程导入与测试思维建立
在 ASP.NET Core 项目中,很多团队第一次写测试时都会遇到同一个问题:测试写了不少,但真正能拦住线上故障的却不多。原因往往不是测试写得不够多,而是测试的层次、职责和投入比例没有设计好。本节先建立一张整体地图,把“测什么、在哪一层测、用什么工具测、质量如何内建到开发过程中”这几件事串起来,后续章节再逐层展开。
一、ASP.NET Core 测试与质量工程的整体地图
一个可交付的 ASP.NET Core 服务,通常由若干可独立观察的部分组成:HTTP 接口层(Controller 或 Minimal API 端点)、应用服务层(业务编排)、领域模型层(实体、值对象、领域规则)、基础设施层(EF Core、缓存、消息、外部 HTTP 客户端)。测试与质量工程要做的,就是为这些部分分别设计合适的验证手段,并让它们在 CI 流水线中自动运行。
从工程视角看,质量工程包含四条并行的主线:测试(单元、集成、端到端)、静态质量(编译警告、分析器、格式检查)、运行时质量(日志、指标、健康检查、分布式追踪)、流程质量(代码评审、CI 门禁、缺陷复盘)。本课程重点放在测试主线,同时把其余三条主线作为测试的输入与输出衔接起来。
二、测试金字塔在 .NET 生态中的映射
测试金字塔的核心主张是:底层单元测试数量最多、执行最快、成本最低;中层集成测试数量适中;顶层端到端测试数量最少、最慢、最脆弱。在 .NET 生态中,这个金字塔有非常具体的落地形态:
- 单元测试层:使用 xUnit / NUnit / MSTest 作为测试框架,Moq 或 NSubstitute 作为替身库,FluentAssertions 提升断言可读性。被测对象是纯逻辑类,例如领域服务、计算器、验证器,不依赖数据库和网络。
- 集成测试层:使用
Microsoft.AspNetCore.Mvc.Testing提供的WebApplicationFactory<TEntryPoint>在内存中启动真实应用管道,配合 Testcontainers 或 SQLite 内存库验证 EF Core 映射、中间件、路由、序列化与数据库交互。 - 端到端测试层:使用 Playwright for .NET 或 Selenium 驱动真实浏览器,验证前后端联调后的关键用户路径,例如登录、下单、支付回调。
需要强调的是,金字塔不是“数量比例”的硬性规定,而是“反馈速度与置信度”的权衡。一个常见的合理起点是:单元测试占 60%~70%,集成测试占 20%~30%,端到端测试占 5%~10%。比例应根据系统风险调整,例如支付类系统应提高集成测试比重,而纯算法库可以几乎全部是单元测试。
三、质量内建理念:把测试左移,而不是堆在发布前
“质量内建”意味着质量不是靠发布前集中测试“测出来”的,而是在需求澄清、接口设计、编码、提交、构建的每个环节逐步“建进去”的。落到 ASP.NET Core 项目,可以具体化为以下做法:
- 需求阶段:把验收标准写成可执行的 Given-When-Then 场景,后续可直接转化为集成测试用例。
- 设计阶段:对依赖外部系统的部分先定义接口(如
IPaymentGateway),使单元测试可以用替身隔离。 - 编码阶段:开启
<Nullable>enable</Nullable>与<TreatWarningsAsErrors>true</TreatWarningsAsErrors>,让编译器先拦住一类缺陷。 - 提交阶段:通过 Git Hook 或 CI 在 PR 上运行单元测试与格式检查,失败即阻断合并。
- 部署阶段:用健康检查端点
/healthz与冒烟测试确认部署结果,而不是只看进程是否启动。
四、课程项目与工具链概览
为了让后续章节有统一的演练对象,本课程使用一个贯穿始终的示例项目 OrderHub:一个简化的订单服务,包含商品查询、下单、库存扣减、支付回调四个核心用例。项目采用 ASP.NET Core 8 Minimal API + EF Core + SQL Server,分层为 OrderHub.Api、OrderHub.Application、OrderHub.Domain、OrderHub.Infrastructure,测试项目为 OrderHub.UnitTests、OrderHub.IntegrationTests、OrderHub.E2ETests。
工具链方面,本课程统一使用以下组合,避免在多个等价工具之间反复切换:
| 用途 | 选型 | 说明 |
|---|---|---|
| 测试框架 | xUnit | 与 .NET 生态集成好,支持 Theory 数据驱动 |
| 替身库 | NSubstitute | 语法简洁,适合接口隔离 |
| 断言库 | FluentAssertions | 失败信息可读性强 |
| 集成测试宿主 | WebApplicationFactory | 内存中启动真实管道 |
| 依赖容器 | Testcontainers | 用真实 SQL Server 容器验证 EF Core |
| 端到端 | Playwright for .NET | 跨浏览器、自动等待 |
| 覆盖率 | Coverlet + ReportGenerator | 生成 HTML 覆盖率报告 |
| CI | GitHub Actions | PR 触发测试与门禁 |
一个典型的测试项目文件配置如下,它决定了测试能否被 CI 正确发现与统计:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7" />
<PackageReference Include="coverlet.collector" Version="6.0.1" />
</ItemGroup>
</Project>
运行测试并收集覆盖率的命令为:
dotnet test OrderHub.UnitTests \
--collect:"XPlat Code Coverage" \
--results-directory ./TestResults
执行后,./TestResults 下会生成 coverage.cobertura.xml,可用 ReportGenerator 转为 HTML:
reportgenerator \
-reports:./TestResults/**/coverage.cobertura.xml \
-targetdir:./coveragereport \
-reporttypes:Html
打开 coveragereport/index.html,即可看到按程序集、类、方法逐级下钻的行覆盖率与分支覆盖率。需要提醒的是,覆盖率是观察指标而非目标本身:一个 90% 覆盖率但断言薄弱的测试套件,其保护能力可能低于 70% 覆盖率但断言精准的套件。后续章节会专门讨论如何判断测试是否“有效”。