Skip to content
bitzorcas
中EN

Reference

集成测试

区分 API Shell 与 Docker 合同,验证 ORM parity、迁移、消息、租户隔离和真实基础设施语义。

Last updated

BitzOrcas.Integration.Tests 同时容纳无容器的 Host/API 合同与 Testcontainers 合同。两者成本、失败模式和证明范围不同,必须通过 Category filter 分流;“都在 Integration.Tests 里”不意味着可以使用同一种夹具或在同一个 CI Job 运行。

证据分层

业务/适配器风险

Category!=Docker

Category=Docker

API Shell、组合根、配置 fail-closed

SQL Server:ORM / schema / transaction

RabbitMQ:CAP / Outbox / handler

可观察结果合同

租户、状态、事件、错误语义

无 Docker 合同验证 ASP.NET Core 组合与跨层控制流,但不声称数据库 provider 已工作。Docker 合同验证真实 adapter 语义,但仍不替代生产容量、灾备或安全测试。

无 Docker 合同

这组测试使用 TestApiFactory 或直接构建应用服务,覆盖 API Shell 启动、认证/租户中间件、Problem Details、健康检查、配置缺失时失败关闭,以及不需要外部 broker/database 的跨层路径。

Terminal window
# 本地快速反馈和普通 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 四种模式)。

Terminal window
# 复现 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。

失败定位顺序

  1. 确认 Docker daemon、磁盘、内存和镜像拉取正常。
  2. 查看容器日志与 readiness,区分启动失败和业务失败。
  3. 确认 schema 初始化、migration 与 seed 报告。
  4. 检查当前分片 filter 是否选择了预期测试。
  5. 再比较 provider 的输入、租户上下文、事务边界和业务断言。
  6. 对 hang 保留 blame 证据,检查等待条件、取消令牌和容器清理。

评审清单

  • Docker 用例带统一 Trait,并且属于唯一 CI 分片;
  • 数据、租户、数据库和队列对测试隔离;
  • provider parity 比较业务语义而不是 SQL 字符串;
  • 最终一致断言使用有上限的轮询而非固定 sleep;
  • migration 覆盖首次执行、升级、重复执行与部分失败;
  • 失败输出含 provider、租户、业务键和容器日志线索;
  • 无 Docker 测试没有夸大成真实基础设施证据。

另见

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%