编写优秀的单元测试
编写优秀的单元测试
备注
太久没写单元测试了
等有时间再补代码用例
对知识点能有更好的补充说明。 mark
简介
单元测试主要是作为一种良好的实践来编写的。
它能帮助开发人员识别并修复bug、重构代码。
- 理想的单元测试应当覆盖程序所有可能的路径
- 一个单元测试通常覆盖一个函数或方法中的一个特定路径
- 测试方法之间经常有隐含的依赖关系暗藏在测试的实现方案中
单元测试的目标是尽可能地隔离周边环境的情况下测试每个组件。
只有隔离了周围环境的影响,才能发现被测试的组件与周边组件间的耦合是否真正被解开。
所以单元测试也可用于检测代码是否过于耦合。
目录
测试进展
对于每个测试时的运行,PHPUnit 命令行输出一个字符来指示进展
字符 | 进展 |
---|---|
. | 测试成功时输出. |
F | 一个断然失败时输出F |
E | 产生一个错误时输出E |
R | 当测试被标记为有风险时输出R |
S | 当测试被跳过时输出S |
I | 测试被标记为不完整或未实现时输出I |
失败是指违背了
PHPUnit
的断言
错误是指意料之外的异常或PHP
错误
这种差异在某些时候是非常有用的,因为错误往往比失败更容易修复
如果得到一个非常长的问题列表,那么最有先对付错误
常用命令
1 | phpunit ArrayTest |
PHPUnit
命令行测试执行器在当前工作目录中寻找ArrayTest.php
源文件并加载之
如果在此源文件中能找到ArrayTest
测试用例类,此类中的测试将被执行
常见问题
Q:当你想把一些东西写到 print
语句或者调试表达式中时
A: 别这么做,将其写成一个测试来代替
基础知识
- 针对类
Class
的测试写在类ClassTest
中 ClassTest
通常继承自PHPUnit\Framework\TestCase
- 测试通常是命名为
test*
的公用(public
)方法 - 在测试方法内,类似于
assertEquals()
这样的断言方法用来对实际值于预期值做出断言
测试的依赖关系
PHPUnit
支持对测试方法之间的显式依赖关系进行声明
这种依赖关系并不是定义在测试方法的执行顺序中
而是允许生产者返回一个测试基境( fixture
) 的示例
并将此实例传递给依赖于它的消费者
生产者( producer ) : 是能生成被测单元并将其作为返回值的测试方法
消费者(consumer) : 是依赖于一个或多个生产者及其返回值的测试方法
@depends
标注来表达依赖情况
默认情况下,生产者所产生的返回值将“原样”传递给相应的消费者。
这意味着如果生产者返回的是一个对象,那么传递给消费者的是一个指向此对象的引用
如果需要传递对象的副本而非引用,则应当用 @depends clone 替代 @depends
数据供给器
测试方法可以接受任意参数
这些参数可以由数据供给器来提供
用 @dataProvider
标注来指定哪个数据供给器方法
数据供给器方法必须声明为 public
其返回值可以是一个数组,其每个元素是一个数组的话,可以利用键值对元素进行说明
或者是一个实现了 lterator
接口的对象。
测试异常
方法:
- expectException()
- expectExceptionCode()
- expectExceptionMessage()
- expectExceptionMessageRegExp()
可以用于为被测代码所抛出的异常建立预期,也可以使用expectedException标注
默认情况下,PHPUnit将测试在执行中触发的PHP错误、警告、通知都转换为异常
所以对异常进行测试是越明确越好,对于太笼统的类进行测试有可能导致不良副作用
对输出进行测试
有时候想要断言某方法的运行过程是否中生成了预期的输出
断言输出的方法:expectOutputString()
获取程序输出的方法:getActualOutput()
基境
在编写试时最费时的部分之一就是编写代码来将整个场景设置成某个已知的状态
并在测试结束后将其复原到初始状态
这个已知的状态称为测试的基境( fixture
)。
PHPUnit 支持共享建立基境的代码
在运行某个测试方法前,会调用一个叫 setUp()
的模板方法
setUp()
是创建测试所用对象的地方
当测试方法运行结束后,不管成功或失败都会调用 tearDown()
方法
tearDown()
是清理测试所用对象的地方
setUp() 多 tearDown() 少
实际上只有在 setUp() 中分配类诸如文件或套接字之类的外部资源才需要实现 tearDown()
如果 setUp() 中只创建纯 PHP 对象,通常可以略过 tearDown()
如果两个基境建立工作略有不同
- 如果两个
setUp()
代码仅有微小差异,把差异代码从setUp()
移到测试方法内 - 如果两个
setUp()
是确实不一样,那么需要另外一个测试用例类
基境共享
一个有实际意义的多测试间共享基境的例子是数据库连接
只登陆数据库一次,然后重用此连接,而不是每个测试都建立一个新的数据库连接
在同一个测试套件内的不同测试之间共享基境
用 setUpBeforeClass()
和 tearDownAfterClass()
模板方法来
分别在测试用例类的第一个测试之前和最后一个测试之后连接与断开数据库
组织测试
我们希望能将任意数量的测试以任何组合方式运行
- 用文件系统来编排测试套件
最简单的大概就是把偶有测试用例源文件放在一个测试目录中
通过对测试目录进行递归遍历,PHPUnit 能自动发现并运行测试
这种方法的缺点是无法控制测试的运行顺序
这可能导致测试依赖关系方面的问题
- 用 XML 配置文件也可以用于编排测试套件
如果phpunit.xml 或 phpunit.xml.dist 存在与当前工作目录且未使用 –configuration
将自动从此文件中读取配置,可以明确指定测试的执行顺序
数据库测试
许多入门与中级的单元测试范例读暗示着这样一种信息
很容易用简单的测试来对应用程序的逻辑进行测试
但对于以数据库为中心的应用程序而言,这与现实想去甚远
难点
为什么所有单元测试的范例都不包含数据交互?
这类测试的建立和维护都很复杂。
对数据库进行测试时,需要考虑以下这些变数 :
- 数据库和表
- 向表中插入测试所需要的行
- 测试运行完毕后验证数据库的状态
- 每个新测试都要清理数据库
另外必须认识到,对于代码而言,本质上来说数据库是全局输入变量
一个测试中出现的失败很容易影响到后继的测试结果,从而让整个测试过程变得非常艰难
随着数据库交互规模的增大,运行测试可能需要耗费可观的时间
只要保持每个测试所使用的数据量较小并且尽可能用非数据库测试来对代码进行测试
数据库测试的四个阶段
- 建立基境
- 执行被测系统
- 验证结果
- 拆除基境
测试替身
有时候对被测系统进行测试是很困难的,因为它依赖于其他无法在测试环境中使用的组件
这有可能是因为这些组件不可用,它们不会返回测试所需要的结果,或者执行它们会有不良副作用
在其他情况下,我们的测试策略要求对被测系统的内部行为有更多控制或更多可见性
如果在编写测试时无法使用(或选择不使用)实际的依赖组件(DOC),可以用测试替身来代替
测试替身不需要和真正依赖最贱有完全一样的行为方式
他只需要提供和真正的组件同样的 API 即可,这样被测系统会以为它是真正的组件
测试实践
你总能编写更多测试。但是很快就会发现,在所有想得出来的测试中只有很小一部分是真正有用的
需要编写的是那些觉得能运作但却失败或觉得必将失败却成功的测试
另一种思考的方式是从成本/收益的关系上去考量,需要编写的是能够给出反馈信息的测试
在开发过程中
当需要对软件的内部结构进行更改时,你实际上是要在不影响其可见行为的情况下让它更加容易理解、更加易于修改
测试套件对于安全地进行这些所谓的重构而言是非常宝贵的,否则,你可能在重构的过程中将系统搞坏而不自知
在使用单元测试来确认重构的转换步骤中确实保持原有行为并且没有引入错误时,以下情况有助于改进项目的编码与设计
- 所有单元测试均正确运行
- 代码传达其设计原则
- 代码没有冗(rong)余
- 代码所包含的类和方法的数量降至最低
在调试过程中
当看到缺陷报告时,你可能会有尽快修复错误的冲动
经验表明,这种冲动不是好事,因为修复一个缺陷很有可能导致另外一个缺陷
下列操作可以帮你压住冲动
- 确认能够重现此缺陷
- 在代码中寻找此缺陷最小规模的表达。例如,如果在输出中有一个数字看起来不对,那么就寻找出算出此数字的那个对象
- 编写一个目前会失败而缺陷修复将会成功的自动测试
- 修复缺陷
寻找缺陷的最小可靠重现使你有机会切真正检查缺陷的原因
当修复了缺陷之后,所编写的测试有助于提高缺陷真正被修复的几率
因为新加入的测试降低了未来修改代码时又破坏此修复的可能性
而之前所编写的所有测试则降低了在不经意间导致其他问题的可能性