9.2 领域逻辑单元测试
单元测试最适合保护不依赖数据库、HTTP 和文件系统的业务规则。它运行快、定位准,是重构时最可靠的安全网。
学习目标
- 能为实体和值对象编写单元测试。
- 能用测试命名表达业务行为。
- 能避免在单元测试中引入外部依赖。
- 能区分单元测试和集成测试的边界。
应用场景
- 任务标题不能为空。
- 已完成任务不能重复完成或重复计数。
- 优先级、截止日期和状态转换规则。
- 价格、折扣、权限等纯业务计算。
示例:测试任务完成规则
public sealed class TodoItemTests
{
[Fact]
public void Complete_MarksTodoAsCompleted_WhenTodoIsOpen()
{
var todo = new TodoItem("学习单元测试");
todo.Complete(new DateTimeOffset(2026, 8, 12, 10, 0, 0, TimeSpan.Zero));
todo.IsCompleted.Should().BeTrue();
todo.CompletedAt.Should().NotBeNull();
}
[Fact]
public void Constructor_ThrowsException_WhenTitleIsEmpty()
{
var act = () => new TodoItem(" ");
act.Should().Throw<ArgumentException>()
.WithMessage("标题不能为空*");
}
}
测试命名建议
| 模式 | 示例 |
|---|---|
| 方法_结果_条件 | Complete_MarksTodoAsCompleted_WhenTodoIsOpen |
| 行为描述 | CannotCreateTodoWithoutTitle |
| 业务规则描述 | CompletedTodoKeepsOriginalCompletedAt |
重点难点
- 单元测试不要连接真实数据库。
- 测试应覆盖边界值,例如空标题、最大长度、重复操作。
- 测试命名要让失败原因一眼可见。
- 业务逻辑如果很难单元测试,通常说明依赖边界需要调整。
常见误区
| 误区 | 推荐做法 |
|---|---|
| 只测试公共 API 成功路径 | 核心业务失败路径也要测 |
| 一个测试断言过多行为 | 一个测试聚焦一个业务事实 |
| 为了测试暴露大量 public set | 通过业务方法驱动状态变化 |
练习
- 为
TodoItem.Rename写空标题和成功重命名测试。 - 为“重复完成不改变 CompletedAt”写测试。
- 把一个依赖系统时间的测试改为传入固定时间。
延伸阅读
- xUnit。
- FluentAssertions。
- 测试金字塔。