其实一开始我内心是想做单元测试(unit testing)的,但时间久了,也就不想了。
要想通过PHP编程成为技术领域的专家,其实功夫在PHP之外。数据库至少得看几本书,xml至少得看一本书,单元测试至少得看一本书,软件工程至少看一本,数据结构与算法至少看一本,*nix至少得看一本,Web服务器至少看一本,佛经得看一本,道德经得看一本,易经可能也得看一本,等等,不停的看下去。
概念
要写单元测试,必须要有一些基本概念。这些概念PHP是不会教给你的。
我们先从百度百科中吸取一点营养。
工厂在组装一台电视机之前,会对每个元件都进行测试,这就是单元测试。
单元测试,是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
单元测试是由程序员自己来完成,最终受益的也是程序员自己。程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。
--百度百科
安装
PHPUnit目前稳定版已到了4.6,网上很多的姿势都已失效。比如,发行方式,以前都是用Pear进行安装,现在呢,直接是Phar包或者Composer的方式依赖安装。
wget https://phar.phpunit.de/phpunit.phar? chmod +x phpunit.phar? sudo mv phpunit.phar /usr/local/bin/phpunit? phpunit --version
{ "require-dev": { "phpunit/phpunit": "4.6.*" }}
但另一个对初学者很关键的东东phpunit-skelgen就没有包含在这个包里,而且很难找到下载地址:https://phar.phpunit.de/phpunit-skelgen.phar
wget https://phar.phpunit.de/phpunit-skelgen.pharchmod +x phpunit-skelgen.pharmv phpunit-skelgen.phar /usr/local/bin/phpunit-skelgen
解决的问题
在开发过程中,当需要对软件的内部结构进行更改时,你实际上是要在不影响其可见行为的情况下让它更加容易理解、更加易于修改,测试套件对于安全地进行这些所谓的重构而言是非常宝贵的。否则,你可能在重组过程中将系统搞坏而不自知。
在使用单元测试来确认重构的转换步骤中确实保持原有行为并且没有引入错误时,以下情况有助于改进项目的编码与设计:
所有单元测试均正确运行。
代码传达其设计原则。
代码没有冗余。
代码所包含的类和方法的数量降至最低。
当需要向系统内添加新的功能时,首先为其编写测试。然后,当测试能够正常运行就标志着开发完成了。
优点
1、它是一种验证行为。
程序中的每一项功能都是测试来验证它的正确性。它为以后的开发提供支援。就算是开发后期,我们也可以轻松的增加功能或更改程序结构,而不用担心这个过程中会破坏重要的东西。而且它为代码的重构提供了保障。这样,我们就可以更自由的对程序进行改进。
2、它是一种设计行为。
编写单元测试将使我们从调用者观察、思考。特别是先写测试(test-first),迫使我们把程序设计成易于调用和可测试的,即迫使我们解除软件中的耦合。
3、它是一种编写文档的行为。
单元测试是一种无价的文档,它是展示函数或类如何使用的最佳文档。这份文档是可编译、可运行的,并且它保持最新,永远与代码同步。
4、它具有回归性。
自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地的快速运行测试。
实践
什么时候测试?
单元测试越早越好,早到什么程度?
极限编程(Extreme Programming,或简称XP)讲究TDD,即测试驱动开发,先编写测试代码,再进行开发。在实际的工作中,可以不必过分强调先什么后什么,重要的是高效和感觉舒适。
从经验来看,先编写产品函数的框架,然后编写测试函数,针对产品函数的功能编写测试用例,然后编写产品函数的代码,每写一个功能点都运行测试,随时补充测试用例。
所谓先编写产品函数的框架,是指先编写函数空的实现,有返回值的直接返回一个合适值,编译通过后再编写测试代码,这时,函数名、参数表、返回类型都应该确定下来了,所编写的测试代码以后需修改的可能性比较小。
由谁测试?
单元测试与其他测试不同,单元测试可看作是编码工作的一部分,应该由程序员完成,也就是说,经过了单元测试的代码才是已完成的代码,提交产品代码时也要同时提交测试代码。测试部门可以作一定程度的审核。
请一定要看完官方文档:
要进行充分的单元测试,一般来说应专门编写测试代码,并与产品代码隔离。但对于初学者来说,总是会有点别扭,因为感觉额外做了很多工作,影响开发效率。其实像phpunit也是支持在类方法的文档注释块(docblock)中使用 @test 标注将其标记为测试方法的。这样,彻底贯彻我们代码即文档的思想。
<?phpclass Calculator{ /** * @assert (0, 0) == 0 * @assert (0, 1) == 1 * @assert (1, 0) == 1 * @assert (1, 1) == 2 * @assert (1, 2) == 4 */ public function add($a, $b) { return $a + $b; }}
现实难题
我们到底要测什么?算法?一般很少。
大都是在编写业务功能。而且大多数是基于数据库的系统开发。这是我们实施PHP单元测试最大的难点所在。需要整合PHPUnit的DBUnit测试,也就是一开始就得学习DBUnit的知识。
在各种编程语言中,许多入门与中级的单元测试范例都暗示着这样一种信息:很容易用简单的测试来对应用程序的逻辑进行测试。但是对于以数据库为中心的应用程序而言,这与现实相去甚远。一旦开始使用诸如 Wordpress、TYPO3、或 Symfony(配合 Doctrine 或 Propel)之类的东西,就很容易在用 PHPUnit 时碰到超多问题:正是由于这些库和数据库之间实在耦合的太紧密了。
你大概会在日常工作面对的项目中经历这一幕。你打算把你那或生疏或纯熟的 PHPUnit 技能用到工作中去,结果被以下问题之一卡住了:
- 待测方法执行了一个相当大的 JOIN 操作,并且得到的数据用于计算某些重要的结果。
- 业务逻辑中混合执行了 SELECT、INSERT、UPDATE 和 DELETE 语句。
- 为了给待测方法建立合理的初始数据,需要在两个以上(可能远超过)表里设置测试数据。
DbUnit
DbUnit扩展大大简化了为测试设置数据库的操作,并且可以在对数据执行了一系列操作之后验证数据库的内容。
DbUnit所支持的供应商
DbUnit 目前支持 MySQL、PostgreSQL、Oracle 和 SQLite。通过集成 Zend Framework 或 Doctrine 2,也可以访问其他数据库系统,比如 IBM DB2 或者 Microsoft SQL Server。
数据库测试的难点
为什么所有单元测试的范例都不包含数据库交互?这里有个很好的理由:这类测试的建立和维护都很复杂。对数据库进行测试时,需要考虑以下这些变数:
- 数据库和表
- 向表中插入测试所需要的行
- 测试运行完毕后验证数据库的状态
- 每个新测试都要清理数据库
许多数据库 API,比如 PDO、MySQLi 或者 OCI8,都十分繁琐且书写起来十分冗长,因此,手工进行这些步骤绝对是噩梦。
测试代码应当尽可能简短精确,这有若干原因:
你不希望因为生产代码的小变更而需要对测试代码进行数量可观的修改。
你希望在哪怕好几个月以后也能轻松地阅读并理解测试代码。
另外,必须认识到,对于代码而言,本质上来说数据库是全局输入变量。测试套件中的两个不同的测试可能是运行在同一个数据库上的,并且可能把数据重用好多次。一个测试中出现的失败很容易影响到后继测试的结果,从而让整个测试过程变得非常艰难。前面提到的清理步骤对于解决“数据库是全局输入”的问题是非常重要的。
DbUnit 以一种优雅的方式来帮助简化数据库测试中的所有这些问题。
PHPUnit 无法帮你解决的问题是,相对于不使用数据的测试而言,数据库测试是非常慢的。随着数据库交互规模的增大,运行测试可能需要耗费可观的时间。然而,只要保持每个测试所使用的数据量较小并且尽可能用非数据库测试来对代码进行测试,即使很大的测试套件也能轻松在一分钟内跑完。
数据库测试的四个阶段
Gerard Meszaros 在他的书《xUnit 测试模式》中列出了单元测试的四个阶段:
- 建立基架(fixture)
- 执行被测系统
- 验证结果
- 拆除基架(fixture)
什么是基架(fixture)?
基架(fixture)是对开始执行某个测试时应用程序和数据库所处初始状态的描述。
对数据库进行测试至少要处理建立与拆除的步骤,在其中完成清理工作,并将所需的基架数据写入表内。然而对于数据库扩展模块而言,在数据库测试中有很好的理由将这四个步骤还原成类似下面这样的工作流程,这个流程对于每个测试都会完整执行:
- 清理数据库
由于总是会有某个测试运行在并不确定表中是否有数据的数据库上,PHPUnit 在所有指定表上执行 TRUNCATE 操作来把它们清空。
- 建立基架
PHPUnit 随后将迭代所有指定的基架数据行并将其插入到对应的表里。
3–5. 运行测试、验证结果、并拆除基架
在所有数据库都完成重置并加载好初始状态后,PHPUnit 才会执行实际的测试。这个部分的测试代码完全不需要数据库扩展模块的参与,可以随意测试任何想要测试的内容。
在测试中,验证的目的可以使用一个名为 assertDataSetsEqual() 的特殊断言来实现。当然,这完全是可选的。
一些术语
- 单元测试
- 集成测试
- 回归测试
- 测试用例
- 断言
- 基架Fixture