单元测试简介
了解 Elixir 中的单元测试。
对于大多数工程师而言,测试体验通常从单元测试开始。单元测试之所以最容易编写,原因如下:
- 聚焦范围有限。
- 比其他类型的测试更简单。
- 构成了所有测试的基础。
在本章中,我们将学习如何定义测试的范围,为功能代码编写测试,并组织测试文件。随后,我们会探讨一些隔离被测代码的方法。
定义单元
单元测试是一种专注于应用程序某一部分的软件测试,通常会移除代码使用时的整体上下文。
常见的做法是将一个单元测试聚焦于单个函数及其内部逻辑,可能还包含该函数可访问的私有函数。有时,将多个模块或处理流程的代码纳入单元测试的范围更有意义,从而扩大了单元的定义范围。
为什么要进行测试?
在设计测试时,我们需要牢记四个目标:
- 证明我们的代码确实实现了预期功能。
- 防止引入破坏性变更(回归)。
- 便于快速定位代码出错的位置。
- 尽可能减少测试代码的编写量。
前两个目标较为常见,而后两个目标将指导我们决定测试范围内应包含多少代码作为“单元”。
我们的测试应在被测代码周围形成一个黑盒,我们称之为“被测代码”或“应用代码”。这个黑盒定义了测试代码与应用代码之间的交互点。在单元测试中,这个黑盒通常将单个函数视为一个单元。
保持测试范围狭窄有利有弊。
| 优点 | 缺点 |
|---|---|
| 快速定位问题所在 | 需要编写更多测试代码来隔离被测代码 |
如果允许测试范围(黑盒)包含已充分测试且功能正常的依赖项,我们可以在享受窄范围优势的同时,避免部分隔离工作的复杂性。
隔离被测代码
我们的测试需要以两种方式与被测代码交互:
- 测试会调用某个函数(或发送消息),传入参数并获取返回值。
- 测试可能需要拦截对外部调用的请求,并返回模拟响应,代替实际依赖项,特别是当我们的代码依赖于代码库中的其他部分时。
第二种交互更为复杂,称为隔离被测代码。
我们将在课程后续部分详细讲解如何隔离代码,但首先,让我们看看在什么情况下可以不必严格隔离。下图展示了围绕被测代码绘制的黑盒:
由于被测代码依赖于代码库中的其他代码,测试必须处理隔离的问题。虽然存在多种隔离方法,但它们通常伴随着代价,主要体现在时间和测试复杂度上。纯函数式代码的存在会影响需要隔离的内容。
纯函数的特性
如果一个函数在每次使用相同参数调用时都返回完全相同的值,并且没有任何副作用,则称其为纯函数。
扩展黑盒范围
当我们的依赖项是纯函数且已独立经过充分测试时,我们可以将该依赖项纳入黑盒范围,如以下示意图所示:
扩展黑盒范围使我们能够省去额外的隔离工作,同时仍能轻松定位问题代码。测试依然聚焦于被测代码本身,而由于被测代码是纯函数,测试环境完全可控,避免了结果不一致的问题。
通过这种方式包含依赖项的好处包括:
- 测试更容易编写、理解和维护。
- 编写的测试代码更少,但仍能在测试失败时准确指出问题所在。
- 如果依赖项自身的测试未失败,但我们在引入该依赖项的测试中出现失败,说明问题出在新代码中,而非依赖项本身。
注意: 关键在于把握何时应将纯函数式依赖项纳入测试范围。这种判断需要通过不断尝试和经验积累来掌握。一种有效策略是先将黑盒范围扩大以包含这些依赖项,若发现调试困难再逐步收紧范围。随着实践增多,我们会逐渐培养出对这一决策的敏锐直觉。