BitzOrcas.Integration.Tests 同时容纳无容器的 Host/API 合同与 Testcontainers 合同。两者成本、失败模式和证明范围不同,必须通过 Category filter 分流;“都在 Integration.Tests 里”不意味着可以使用同一种夹具或在同一个 CI Job 运行。
证据分层
无 Docker 合同验证 ASP.NET Core 组合与跨层控制流,但不声称数据库 provider 已工作。Docker 合同验证真实 adapter 语义,但仍不替代生产容量、灾备或安全测试。
无 Docker 合同
这组测试使用 TestApiFactory 或直接构建应用服务,覆盖 API Shell 启动、认证/租户中间件、Problem Details、健康检查、配置缺失时失败关闭,以及不需要外部 broker/database 的跨层路径。
# 本地快速反馈和普通 PR 门禁;明确排除所有 Testcontainers 用例。dotnet test tests/BitzOrcas.Integration.Tests \ --configuration Release \ --filter 'Category!=Docker'
# 只复现 API Shell 构建冒烟测试。dotnet test tests/BitzOrcas.Integration.Tests \ --configuration Release \ --filter 'FullyQualifiedName~ApiShellHostBuildSmokeTests'适合放在这里的断言包括:端点是否映射、未认证响应协议、租户上下文能否投影、缺失生产配置是否阻止启动。SQL 唯一索引、事务回滚、锁与消息投递必须转入 Docker 合同。
Docker 合同与 Trait
真实基础设施测试使用 [Trait(TestCategories.CategoryKey, TestCategories.Docker)]。新增容器用例若漏 Trait,会混入无 Docker Job;DockerContractTraitTests 会守住这项分类合同。
[Trait(TestCategories.CategoryKey, TestCategories.Docker)]public sealed class WebsitePersistenceProviderContractTests : IAsyncLifetime{ // 类级生命周期让同一合同共享容器;测试方法仍各自创建数据。 public Task InitializeAsync() => StartSqlServerAndCreateSchemaAsync();
[Fact] public async Task DuplicateSlug_ShouldMapToSameConflict_ForBothProviders() { // 用同一业务情景分别驱动 EF Core 与 SqlSugar,不比较 SQL 文本。 var efResult = await ExecuteDuplicateSlugAsync(PersistenceProvider.EfCore); var sqlSugarResult = await ExecuteDuplicateSlugAsync(PersistenceProvider.SqlSugar);
efResult.ErrorCode.ShouldBe("Website.SlugConflict"); sqlSugarResult.ErrorCode.ShouldBe(efResult.ErrorCode); }
public Task DisposeAsync() => StopContainersAsync();}示例强调合同形态;仓库里的 Website 合同还会验证生产 metadata、唯一索引、schema reset 和真实 IEntitySet。不要把 provider parity 降格成“两个 ORM 都没有抛异常”。
ORM parity 应比较什么
Parity 的对象是可观察语义,不是生成 SQL 的字符相同:
| 场景 | 两个 provider 必须一致的结果 |
|---|---|
| 租户读取 | 不返回其他租户数据,缺失租户时 fail-closed |
| 查询形状 | filter、排序、分页、投影与总数 |
| 聚合写入 | 状态、并发版本、审计字段、领域事件 |
| 唯一冲突 | 相同稳定业务错误,而非泄漏 provider 异常 |
| 软删除 | 默认查询不可见,管理路径按合同可见 |
| 事务 | 成功全部提交,失败不留下半成品 |
| 迁移 | 旧数据可升级,重复执行保持幂等 |
只有 provider 特有能力才允许差异,而且差异必须在 adapter 合同和文档中显式说明。
消息与 Golden Use Case
GoldenUseCaseApiFactory 会组合 SQL Server、RabbitMQ 和真实 API Host。Golden Use Case 与 CAP/Outbox 合同关注“请求提交事务后,消息最终由正确 handler 消费”,而不是仅断言内存事件集合包含一项。
// Arrange:为本测试建立独立租户与业务键,避免依赖执行顺序。var client = factory.CreateTenantClient(tenantId);
// Act:命中真实 Golden Use Case 路由 /api/notes——一次请求同时写库并产生集成事件。var noteTitle = "matter-kickoff-note";var response = await client.PostAsJsonAsync("/api/notes", new { title = noteTitle });response.StatusCode.ShouldBe(HttpStatusCode.Created);
// Assert:轮询消息探针直到 CAP 从 Outbox 消费完成;设明确超时,不用 Thread.Sleep 固定等待。await EventuallyAsync( () => messageProbe.ContainsAsync(noteTitle), timeout: TimeSpan.FromSeconds(20));消息测试同时检查重复投递、handler 失败重试、取消与超时。若只验证 happy path,无法证明幂等和恢复能力。
CI 的 13 个分片
.github/workflows/docker-integration-contracts.yml 的 matrix 把 Docker 合同拆成 13 个分片:shared-port-a/b/c/d、shared-repository、shared-readmodel-queryshape、shared-generic、website、webhooks、messaging、authorization-capacity、core、workflow。该 workflow 以 workflow_call/workflow_dispatch 触发(由 CI 或发布流程调用,不在每次 push/PR 常驻),fail-fast: false 保证单分片失败不取消其余;本地等价入口是 scripts/build/test-docker.sh(list/smoke/shard/full 四种模式)。
# 复现 website 分片;应直接复制 CI 中的 filter。dotnet test tests/BitzOrcas.Integration.Tests \ --configuration Release \ --filter 'Category=Docker&FullyQualifiedName~BitzOrcas.Integration.Tests.Website.' \ --blame-hang-timeout 10m新增 Docker 测试必须进入且只进入一个分片,并让 Architecture.Tests 的分片归属合同保持一致。core 使用排除式 filter,命名空间变化可能造成重叠或漏跑,新增时优先考虑显式命名空间。
隔离与生命周期
程序集级 CollectionBehavior(DisableTestParallelization = true) 当前关闭并行,避免共享 Docker 资源争用。测试仍应使用唯一数据库名、租户、业务键和队列标识,不依赖这个开关掩盖数据污染。清理放在 DisposeAsync,即使断言失败也能释放容器。
固定端口容易与开发环境冲突,优先使用 Testcontainers 动态绑定;就绪检查必须等待服务真的可用,不能只看容器状态为 Running。测试数据不得依赖另一测试预先 seed。
失败定位顺序
- 确认 Docker daemon、磁盘、内存和镜像拉取正常。
- 查看容器日志与 readiness,区分启动失败和业务失败。
- 确认 schema 初始化、migration 与 seed 报告。
- 检查当前分片 filter 是否选择了预期测试。
- 再比较 provider 的输入、租户上下文、事务边界和业务断言。
- 对 hang 保留 blame 证据,检查等待条件、取消令牌和容器清理。
评审清单
- Docker 用例带统一 Trait,并且属于唯一 CI 分片;
- 数据、租户、数据库和队列对测试隔离;
- provider parity 比较业务语义而不是 SQL 字符串;
- 最终一致断言使用有上限的轮询而非固定 sleep;
- migration 覆盖首次执行、升级、重复执行与部分失败;
- 失败输出含 provider、租户、业务键和容器日志线索;
- 无 Docker 测试没有夸大成真实基础设施证据。