Skip to content
bitzorcas
中EN

Guide

单元测试

为聚合、值对象、Result 错误、请求规则与应用管线编写快速、确定且能表达业务合同的测试。

Last updated

单元测试负责以最低成本穷举业务分支。它应在没有数据库、消息代理、Redis、网络和真实系统时钟的情况下运行,并通过公开行为表达规则。BitzOrcas 把纯基础/领域测试放在 BitzOrcas.Unit.Tests,把 Handler、授权策略和 Mediator pipeline 等应用控制流放在 BitzOrcas.Application.Tests。

边界判断

否是聚合/值对象/算法Handler/Rule/Pipeline

待验证事实

需要真实适配器语义吗?

Unit / Application

Integration

纯领域还是应用编排?

Unit.Tests

Application.Tests

Testcontainers / API Host

需要确认 SQL 翻译、索引冲突、事务、CAP、序列化协议或中间件顺序时,就已经越过单元边界。不要用 EF InMemory 或一个 List<T> 仓储冒充 provider 合同。

优先测试的行为

类型高价值断言低价值替代
聚合合法状态转换、拒绝分支、领域事件属性 getter
值对象规范化、相等性、边界和错误逐行复制实现算法
Result<T>稳定错误码、错误类型、是否短路只断言 IsFailure
请求规则tenant-specific 规则、取消、组合顺序mock 私有方法
Handlerport 调用后的可观察结果与失败映射每个 mock 调用次数
Pipeline授权/校验/事务等是否按合同短路测框架 DI 本身
纯算法配额、时间窗口、掩码、分页用真实时钟和随机数

测试名应包含场景和结果,例如 Should_ShortCircuit_With_Failure_And_Skip_Handler_When_Rule_Fails。失败日志不打开源码也能看懂预期合同。

聚合相等性与领域事件

仓库的 EntityEqualityTests 不是只测 Equals 的 happy path,还区分相同类型/ID、不同 ID、不同实体类型、null,并验证领域事件收集和清空。

[Fact]
public void SameIdAndType_ShouldBeEqual_ButDifferentTypeShouldNot()
{
// Arrange:固定同一 ID,隔离“标识相同”和“运行时类型相同”两个条件。
var id = Guid.CreateVersion7();
var first = new SampleEntity(id);
var sameType = new SampleEntity(id);
var otherType = new OtherEntity(id);
// Assert:相等运算符、Equals 与 hash code 必须保持同一合同。
first.Equals(sameType).ShouldBeTrue();
(first == sameType).ShouldBeTrue();
first.GetHashCode().ShouldBe(sameType.GetHashCode());
// 同 ID 不足以跨实体类型判等,否则集合和仓储 identity map 会混淆。
first.Equals(otherType).ShouldBeFalse();
}

当聚合行为产生事件时,应断言事件类型和关键业务字段,而不是只断言 Count == 1。如果合同要求失败不产生事件,也要覆盖拒绝路径。

[Fact]
public void RaiseAndClear_ShouldMaintainDomainEventCollection()
{
// 新聚合开始时不应携带任何待分发事件。
var entity = new SampleEntity(Guid.CreateVersion7());
entity.DomainEvents.ShouldBeEmpty();
entity.Raise(new SampleEvent());
entity.DomainEvents.ShouldHaveSingleItem();
// Clear 用于持久化/分发后的生命周期;重复清空仍应安全。
entity.ClearDomainEvents();
entity.ClearDomainEvents();
entity.DomainEvents.ShouldBeEmpty();
}

应用 Pipeline 的短路合同

ValidationPipelineBehaviorTests 使用真实 pipeline 类和小型 IRequestRule<T>,只替代当前租户与策略端口。关键断言不是“规则被调用一次”,而是失败时 handler 不运行、错误码稳定,成功时 handler 才运行。

[Fact]
public async Task FailingRule_ShouldSkipHandler_AndReturnStableError()
{
// Arrange:策略返回一个确定失败的规则,不启动 Host 或数据库。
var pipeline = CreatePipeline(new FailingRule());
var handlerCalled = false;
MessageHandlerDelegate<CreateGreetingCommand, Result<Greeting>> next = (_, _) =>
{
handlerCalled = true;
return ValueTask.FromResult(Result<Greeting>.Success(CreateGreeting()));
};
// Act:从公开 Handle 合同驱动完整校验控制流。
var result = await pipeline.Handle(
new CreateGreetingCommand("name"), next, CancellationToken.None);
// Assert:既要验证类型化失败,也要证明副作用边界未被越过。
result.Error.Code.ShouldBe("Greeting.Invalid");
handlerCalled.ShouldBeFalse();
}

租户特定规则要至少覆盖:目标租户获得附加规则、其他租户只用基础规则、无租户时的 fail-closed/默认行为(按具体合同)。不要用一个全局静态 tenant 在测试间共享。

替身使用原则

替代真正的外部端口,而不是被测对象的内部方法。常见合理替身包括 ICurrentTenant、固定时钟、ID 生成器、授权策略、通知/仓储 port。仓储 fake 必须只用于 Handler 控制流;若要证明 provider 的并发、过滤或事务,转到集成测试。

// 当前租户是安全相关输入,使用精确值而不是任意参数匹配。
var currentTenant = Substitute.For<ICurrentTenant>();
currentTenant.Tenant.Returns(new CurrentTenant("TENANT_A"));
// 策略只返回当前场景需要的一条通过规则。
var strategy = Substitute.For<IValidationStrategy>();
strategy.GetRules<CreateGreetingCommand>(currentTenant)
.Returns(new IRequestRule<CreateGreetingCommand>[] { new PassingRule() });

避免 ReturnsForAll、任意参数匹配和几十个无意义 setup;它们会让错误调用也通过。对有安全意义的 tenant、resource、error code 使用精确值。

时间、随机与并发

业务规则依赖当前时间时注入固定时钟,断言边界前、边界点和边界后。依赖 ID 时测试只关心唯一性就用可预测 generator;关心 UUID v7 排序则显式构造场景。不要用 Task.Delay 证明超时,使用可控信号和取消令牌。

并发正确性通常依赖真实数据库版本列、唯一索引或锁,属于集成测试。单元层可以验证冲突 Result 的映射逻辑,但不能声称已经证明两个事务竞争。

参数化边界

[Theory]
[InlineData(0, false)]
[InlineData(1, true)]
[InlineData(100, true)]
[InlineData(101, false)]
public void BatchSize_ShouldRespectInclusiveRange(int value, bool expected)
{
// Theory 把边界表变成可读合同,并为每个输入生成独立失败结果。
// 断言直接比较布尔合同,失败日志会同时显示输入与期望。
BatchPolicy.IsAllowed(value).ShouldBe(expected);
}

不要为了减少行数把互不相关的业务场景塞进一个 Theory。每行数据应共享同一规则与失败解释。

运行与诊断

Terminal window
# 两个层级分别运行,便于定位归属。
dotnet test tests/BitzOrcas.Unit.Tests --configuration Release
dotnet test tests/BitzOrcas.Application.Tests --configuration Release
# 修改 Validation pipeline 时先缩小到一个类。
dotnet test tests/BitzOrcas.Application.Tests \
--configuration Release \
--filter 'FullyQualifiedName~ValidationPipelineBehaviorTests'

出现偶发失败时先寻找真实时间、随机值、静态状态、共享集合和执行顺序依赖。不要通过重试或放宽断言把非确定性永久化。

评审清单

  • 测试名表达业务场景与可观察结果;
  • Arrange 只创建当前合同需要的状态;
  • 成功、拒绝、边界和取消路径均按风险覆盖;
  • 错误断言包含稳定 code/type,不依赖易变消息全文;
  • 替身位于外部 port,未暴露私有实现;
  • 测试不依赖网络、真实时钟、固定端口和执行顺序;
  • provider/事务/消息语义没有被 fake 夸大;
  • 修复前测试能红,修复后才绿。

另见

100%

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