测试夹具负责建立边界,种子数据负责建立可重复的初始事实。不要让所有测试共享一个“万能数据库”:纯领域规则使用对象工厂,HTTP 管线使用 WebApplicationFactory,真实持久化与消息语义使用 Testcontainers,平台初始化才使用模块自有的 ISeedStep。
夹具选择矩阵
| 要验证的事实 | 推荐夹具 | 不推荐 |
|---|---|---|
| 聚合状态转换 | 直接构造对象/测试 Builder | 启动 Host |
| Handler 与纯应用策略 | 明确的 fake port、固定时钟 | Mock 私有方法 |
| 中间件、认证、Problem Details | TestApiFactory / WebApplicationFactory | 只调用 Handler |
| SQL 翻译、事务、并发 | Testcontainers SQL Server | EF InMemory |
| CAP/Outbox 与 broker | SQL Server + RabbitMQ 容器 | 内存事件列表 |
| ORM parity | 同一合同分别驱动 SqlSugar/EF Core | 比较生成 SQL 字符串 |
| 包消费 | 临时目录 + 隔离 NuGet Feed/cache | 产品仓库 ProjectReference |
测试类型决定成本与证据,不是“集成测试越多越好”。在最低成本层枚举分支,在高层保留跨边界的关键路径。
当前集成夹具形态
无 Docker API 测试通过 TestApiFactory 启动 Shell 组合;Golden Use Case 的 GoldenUseCaseApiFactory 同时启动 SQL Server、RabbitMQ 与真实 API Host。Store、Repository、Website、CAP、审计和 Seeder 合同通常直接实现 IAsyncLifetime 管理容器。
tests/BitzOrcas.Integration.Tests/AssemblyInfo.cs 关闭测试并行,避免多个容器竞争 Docker 资源产生瞬态失败。不要在单个类中重新开启并行来追求表面速度。
# 无 Docker:验证 API Shell 与跨层安全降级。dotnet test tests/BitzOrcas.Integration.Tests \ --configuration Release --filter 'Category!=Docker'
# 有 Docker:只运行 Seeder 合同,避免启动无关容器。dotnet test tests/BitzOrcas.Integration.Tests \ --configuration Release \ --filter 'Category=Docker&FullyQualifiedName~Seeders.'种子资产归属
种子 CSV 跟随 owner 模块的 Infrastructure 程序集。当前资产分布在 Identity、MasterData、Authorization、Menu、PlatformBilling、Catalog、Numbering 与少量 Framework 兼容位置;不能再用一个集中 SqlSugar 目录表示全部事实。
# 物理资产清单;执行集合仍取决于目标 Host 的 DI 组合。find src/Platform src/Framework \ -path '*/Seeders/Assets/*.csv' -print | sort
# 查看步骤身份、顺序、范围和显式依赖。rg -n 'SeedId =>|Order =>|SeedScope =>|DependsOn =>' \ src/Platform src/Framework -g '*SeedStep.cs'文件名前缀用于阅读和对齐 Order,但 SeedRunner 最终以已注册 ISeedStep.Order 调度。SeedId 或 Order 重复、DependsOn 缺失都会在执行前 fail loud。
ORM 中立种子基类
标准实现是 EntitySetCsvSeedStepBase<TEntity>,不是旧文档中的 CsvSeedStepBase<T>。它从步骤所属程序集读取嵌入 CSV,通过 IEntitySet<TEntity> 按稳定业务键查询,并新增或复制 owner 管理字段。
public sealed class TestCatalogSeedStep( IEntitySet<TestCatalogRecord> rows, CsvSeedReader reader, ILogger<TestCatalogSeedStep> logger) : EntitySetCsvSeedStepBase<TestCatalogRecord>(rows, reader, logger){ public override int Order => 960; public override string SeedId => "test_catalog"; public override SeedScope SeedScope => SeedScope.Demo; protected override string CsvFileName => "960-test_catalog.csv";
// Code 是跨重复运行稳定的业务键,数据库生成 Id 不能承担匹配职责。 protected override string[] WhereColumns => [nameof(TestCatalogRecord.Code)]; protected override Expression<Func<TestCatalogRecord, bool>> Match(TestCatalogRecord source) => row => row.Code == source.Code;
protected override void Copy(TestCatalogRecord source, TestCatalogRecord target) { // 只复制 CSV 拥有的字段;保留持久 Id、租户和运行时审计字段。 target.Name = source.Name; target.IsEnabled = source.IsEnabled; }}示例 Order 必须在实际目标组合中重新检查全局唯一性。业务模块应选择自己的区间并通过架构/集成测试守护,而不是照抄 960。
Scope 与环境
| Scope | Development/Demo | Staging/Production | 用途 |
|---|---|---|---|
Global | 执行 | 执行 | 语言、币种、国家/地区等平台级基础数据 |
Tenant | 执行 | 执行 | 默认租户组织、设置覆盖等租户级数据 |
ProductionSafe | 执行 | 执行 | 必需平台定义、主数据 |
Demo | 执行 | 自动跳过 | 示例目录、非生产账号/内容 |
BitzOrcasSeedOptions 还可以整体禁用 Runner、设置 SkipSeedIds 或配置 CSV override root。测试若使用 override,必须把覆盖目录放在临时空间,并在结束时删除;生产测试证据不能依赖开发机上的未提交 CSV。
确定性数据规则
- 使用测试自有 TenantId、业务键和时间戳,不依赖另一个用例先运行;
- 时间从
IAppClock或显式参数进入,断言不要读取瞬时UtcNow; - 随机数据必须记录 seed,失败可重放;
- 每个测试创建并清理自己的数据库事实,或在事务/独立容器中隔离;
- 断言业务键和可观察结果,不依赖数据库自增值的具体数字;
- 并发测试用同步屏障制造冲突,不用
Task.Delay猜时序; - CSV 不包含真实密码、Token、License、客户数据或本机绝对路径。
Testcontainers 生命周期
public sealed class CatalogSqlContractTests : IAsyncLifetime{ private readonly MsSqlContainer database = new MsSqlBuilder().Build();
public async Task InitializeAsync() { // StartAsync 完成后仍执行查询级 readiness,避免“端口已开但 SQL 未就绪”。 await database.StartAsync(); await MsSqlContainerReadiness.WaitForQueryAsync(database.GetConnectionString()); }
// 即使断言失败,xUnit 仍通过异步释放回收容器资源。 public Task DisposeAsync() => database.DisposeAsync().AsTask();}发生失败时保留容器日志、连接就绪阶段和首个业务断言。不要只把 Testcontainers 超时扩大到几十分钟;先区分镜像拉取、daemon、资源、readiness 与 schema 问题。
幂等与部分失败测试
一个种子步骤至少验证:空库首次执行、重复执行、CSV 管理字段更新、业务键重复被拒绝、缺失依赖被拒绝、Demo 在生产环境跳过。有关联父子步骤时,还要验证 DependsOn 和外键顺序。
“执行两次没有异常”不够:第二次后行数不增加,owner 管理字段收敛,运行时字段不被覆盖,报告状态和退出码都应正确。
夹具评审清单
- 夹具层级与要证明的事实匹配;
- Docker 用例标记
Category=Docker并进入正确 CI shard; - 测试不依赖顺序、固定端口、共享用户或历史数据库;
- 容器有 readiness、取消和确定清理;
- SeedId/Order/DependsOn 在目标组合中唯一且完整;
- Global/Tenant/Demo/ProductionSafe 范围符合数据性质;
- 重放、部分失败和并发路径有断言;
- 日志与失败消息不暴露连接串或 Secret。