Unit Testing Principles, Practices, and Patterns
单元测试概述
单元测试的目标:使软件项目能够 可持续 的迭代,尤其是业务逻辑复杂,周期长的企业级应用。
好的测试应当有如下特征:
- 被集成进开发周期
- 聚焦核心领域部分
- 最小的维护成本和最大的价值
单元测试的定义:
- 验证一小块代码
- 快速执行
- 独立隔离运行
测试不应该去验证 代码单元 ,而应该是去验证 行为单元 ,对领域有意义的事情。
一个测试应该讲述一个可观测的,有意义的 故事。
London 和 Classical 风格的区别
School | Isolation of | A unit is | Uses test doubles for |
---|---|---|---|
London | Units | A class | All but immutable dependencies |
Classical | Unit tests | A class or a set of classes | Shared dependencies |
TDD Test-driven development, 在你的测试中 重复 3个步骤:
- 写一个失败的测试,表明你需要添加的功能和如何表现。
- 编写足够的代码去让测试通过,这一步骤,并不需要巧妙,干净的代码。
- 在单元测试的保护下重构,保证代码的可读和可维护性。
单元测试的最佳实践
AAA模式
public class CalculatorTests
{
[Fact]
public void Sum_of_two_numbers()
{
// Arrange
// 准备待测试想系统(SUT)和它的依赖的期望状态。
double first = 10;
double second = 20;
var calculator = new Calculator();
// Act
// 调用SUT的方法,传入准备好的参数,捕获输出结果。
double result = calculator.Sum(first, second);
// Assert
// 验证输出
Assert.Equal(30, result);
}
}
命名
[MethodUnderTest][Scenario][ExpectedResult] 方法名_场景_期望结果
好的单元测试的四个特性
- 防止BUG的产生
- 耐重构
- 快速反馈
- 可维护
耐重构,要求代码注重 可观测行为 而非 实现细节,与具体实现解耦,相对于白盒测试,倾向于黑盒测试。
Mock
Mock: 向外输出交互(outcoming)。 Stub: 向内输入数据(incoming)。
作者偏向只在集成测试中使用mocks,单元测试不应该使用。因为理想模式下,领域负责处理复杂,控制器负责处理交互。 使用第三方库应当编写自己的适配器,并且去mock这些适配器,而不是直接去mock。
六边形架构
- 领域层和应用服务层关注点分离
- 应用内部,服务层单向依赖领域层
- 应用间只通过服务层沟通
三种测试风格
- 基于输出
- 基于状态
- 基于交互
代码价值
基于复杂度领域价值,和合作者数量划分为4种类型。 其中左上的代码是最具有测试价值的部分。
使用 是否能执行/执行模式(CanExecute/Execute pattern) 来减轻控制器的复杂度。
集成测试
不符合单元测试任一条特性的测试都可以被称作集成测试,集成测试一般测试happy path。
外部依赖分为:
- 可控依赖(Managed dependencies):只用于应用内部交互,典型的比如数据库。
- 不可控依赖(Unmanaged dependencies):应用外部可观测的交互,比如第三方系统。 可控依赖使用真实实体,不可控依赖使用mocks。
系统应当尽可能减少分层,大多数系统可分为 domain model, service layer(controller), infrastructure layer。
All problems in computer science can be solved by another layer of indirection, except for the problem of too many layers of indirection. — David J. Wheeler
测试反模式
- 测试私有方法
- 暴露私有状态
- 泄露领域知识(实现细节)
- mock具体的类