PHPUnit单元测试(Lumen框架)指南
综述
使用 PHPUnit 进行单元测试可参看 PHPUnit5.0 手册。由于手册内容较多,刚接触时有些地方不易理解,这里针对 PHPUnit 内容进行通俗的归纳、及关键点的解释,帮助快速入门。
为什么要进行单元测试?
1.帮助理解需求
单元测试应该反映 Use Case,把被测单元当成黑盒测试其外部行为。
2.提高实现质量
单元测试不保证程序做正确的事,但能帮助保证程序正确地做事,从而提高实现质量。
3.测试成本低
相比集成测试、验收测试,单元测试所依赖的外部环境少,自动化程度高,时间短,节约了测试成本。
4.反馈速度快
单元测试提供快速反馈,把bug消灭在开发阶段,减少问题流到集成测试、验收测试和用户,降低了软件质量控制的成本。
5.利于重构
由于有单元测试作为回归测试用例,有助于预防在重构过程中引入 bug。
6.文档作用
单元测试提供了被测单元的使用场景,起到了使用文档的作用。
7.对设计的反馈
一个模块很难进行单元测试通常是不良设计的信号,单元测试可以反过来指导设计出高内聚、低耦合的模块。
经验表明一个尽责的单元测试方法将会在软件开发的某个阶段发现很多的 Bug,并且修改它们的成本也很低。在软件开发的后期阶段,Bug 的发现并修改将会变得更加困难,并要消耗大量的时间和开发费用。无论什么时候作出修改都要进行完整的回归测试,在生命周期中尽早地对软件产品进行测试将使效率和质量得到最好的保证。 在提供了经过测试的单元的情况下,系统集成过程将会大大地简化。开发人员可以将精力集中在单元之间的交互作用和全局的功能实现上,而不是陷入充满很多 Bug 的单元之中不能自拔。
使测试工作的效力发挥到最大化的关键在于选择正确的测试策略,这其中包含了完全的单元测试的概念,以及对测试过程的良好的管理,还有适当地使用象 AdaTEST 和 Cantata 这样的工具来支持测试过程。这些活动可以产生这样的结果:在花费更低的开发费用的情况下得到更稳定的软件。更进一步的好处是简化了维护过程并降低了生命周期的费用。有效的单元测试是推行全局质量文化的一部分,而这种质量文化将会为软件开发者带来无限的商机。
== 环境配置 ==
=== 需求 === PHPUnit 5.0 需要 PHP 5.6。 代码覆盖率分析报告功能需要 Xdebug (2.1.3以上)与 tokenizer 扩展。生成 XML 格式的报告需要有 xmlwriter 扩展。 === 安装 === 安装请使用PHP 档案包 (PHAR)安装,具体过程参见PHPUnit手册: https://phpunit.de/manual/current/zh_cn/installation.html 注:网上很多介绍使用pear安装的PHPUnit的方法现在已失效(2014年失效)。== Lumen框架中使用PHPUnit ==
=== Lumen框架对PHPUnit的支持 === Lumen 框架对 PHPUnit 提供了非常好的支持与封装,在 Lumen 框架中进行单元测试的操作涉及根目录的 tests 文件夹、 phpunit.xml 及 database 文件夹。 ==== phpunit.xml ==== phpunit.xml 中是对测试环境的配置,下面根据具体配置代码讲解关键点:=====标签===== 中是测试套件的配置,带有一个或多个 子元素的 元素用于将测试套件及测试用例组合出新的测试套件。 可以用 phpVersion 和 phpVersionOperator 属性来指定 PHP 版本需求。在以下例子中,仅当 PHP版本至少为 5.3.0 时才会将 /path/to/*Test.php 文件与 /path/to/MyTest.php 文件添加到测试套件中。./tests ./app/Http/Modules/... ./app/Http/Models ./app/Http/Modules/... 数据库入口配置-->
:suffix=”Test.php” 指明了测试文件的后缀名,因此测试文件必须以 Test.php 结尾,例如 ExampleTest.php ,否则测试文件不会被执行。
/path/to/files /path/to/MyTest.php
=====filter标签=====
filter 标签元素及其子元素用于为代码覆盖率报告配置黑名单 blacklist与白名单 whitelist 。白名单中的文件会产生代码覆盖率报告,当黑名单与白名单冲突时白名单权限更高。
===== 测试数据库入口配置 =====
测试时的数据库入口配置可以通过全局常量 APP_ENV 进行设置,然后在 config 文件夹下的 database.php 中通过全局常量 APP_ENV 跳转相应的数据库连接信息。
*需要注意的是这里的 APP_ENV 的 value 通常取为 ‘testing’ ,这是因为 lumen 对 PHPUnit 进行封装的 TestCase 类中的 refreshApplication() 函数中设置了 putenv(‘APP_ENV=testing’) ,如果需要修改为其他值,需要将两处改为一致的,否则无法正确连接数据库。
protected function refreshApplication() { putenv('APP_ENV=testing'); Facade::clearResolvedInstances(); $this->app = $this->createApplication(); }
===== Logging (日志记录)=====
logging 元素及其 log 子元素用于配置测试执行期间的日志记录。例如下面的例子将会在项目根目录下生产report文件夹,并以html格式存储日志。日志的存储格式可以有多种,具体可参见PHPUnit手册:` https://phpunit.de/manual/current/zh_cn/logging.html 。
==== 使用 factory 填充数据 ====
:在编写测试用例的时候往往需要向数据库中填充铺垫数据,Lumen 提供了很好的填充工具: factory ,根目录下的 database 文件夹下的 factories 中的 ModelFactory.php 中可以设置填充的模型。例如下面的例子为 App\User 模型类设置填充数据的模型工厂,在这里可以使用 Faker 产生随机数据方便填充,使用 Faker 产生各种随机数据可参看 https://github.com/fzaninotto/Faker 。
$factory->define(App\User::class, function (Faker\Generator $faker) { return [ 'name' => $faker->name, 'email' => $faker->email, 'password' => bcrypt(str_random(10)), 'remember_token' => str_random(10), ]; });
在 ModelFactory.php 中设置了数据填充模型之后,在编写测试时如果需要向数据库相应表中填充数据,可以使用 create() 方法,调用方式如下:
factory('App\User')->create();
下面的例子将向表中插入10条数据,并在插入前修改插入数据关键字的值。
factory('App\User', 10)->create(['name' => 'Tom']);
make 方法可以产生数据,但不插入数据库。
$user = factory('App\User')->make(['name' => 'Tom']);
如果需要获得插入数据库后的主键 id 或其他信息可以这样获得:
$id = factory('App\User')->make(['name' => 'Tom'])['attributes']['id'];
==== 每次测试结束后重置数据库 ====
在每次测试结束后都需要对数据进行重置,这样前面的测试数据就不会干扰到后面的测试。
方法是通过使用迁移 DatabaseMigrations 或数据库事务 DatabaseTransactions 。
可参考 https://lumen.laravel-china.org/docs/5.1/testing
*这里需要注意的是 Lumen 对数据库的 DatabaseTransactions 封装只能在连接一个数据库时可以正常工作(既 DatabaseTransactions trait 在测试结束时自动回滚重置数据库只能重置一个数据库,如果连接多个数据库则只有第一个数据库可以正常回滚,其他的无法回滚,数据将被写入数据库,这种现象在我们 KSong、Party 用的 lumen 的版本是存在的,暂未找到解决方法,只好手动切数据库,其他版本有没有修复这个问题还待验证)
=== 测试示例 ===
当同一个测试类中测试的接口较多时,每个接口测试都要插入数据,当测试接口多到一定程度时,在使用过程中会出现数据库中个别表在某个接口中插入数据时等待超时无法插入数据,这个问题暂未能完美的解决,当前的处理方法有两种,一种是调整测试接口执行顺序或将一个类中的接口分写到不同的 php 文件中控制单个测试类内测试接口的数量;一种是以要测试的类为“单元”,将类内接口方法的测试所需的所有数据一次性插入数据库(设计测试铺垫数据时,需要注意各个接口之间的数据不要互相干扰),然后在测试类方法中分别调用测试的接口的断言方法。示例如下例2。
testExample) { $this->testExample = new Example(); } factory('APP/User')->create(); //若在此处插入数据,则当前测试类中的每个测试执行前都将插入数据,并在结束时删除 } public function ExampleProvider() //产生若干条测试用例输入及期望输出 { $inputParam = ['name' => 'Tom', 'name' => ' ', ...]; $expectOut = ['Tom', false, ...]; for($i = 0, $i < count($inputParam), $i++) { data = [$inputParam[$i], $expectOut[$i]]; } return data; } /** * @dataProvider ExampleProvider //这里指明了运行下面测试方法的数据提供源为ExampleProvider的输出 */ public function testMethod($inputParam, $expectOut) //测试方法名必须以test开头,通常test后面跟要测试的方法名称(非必须)。 { factory('APP/User')->create(); . . //向数据库插入若干铺垫数据 . $this->assertEquals($expectOut, $this->testExample->method($inputParam)); //断言输出 } ... //其他测试方法 }
=== 运行测试 ===
由于在 phpunit.xml 中已经设置了测试运行的环境与规则,如果要运行 tests 文件夹下的所有测试,只需在项目根目录里使用命令行: phpunit 便会自动运行所有测试。若要运行单个测试文件只需在后面指明路径即可。
如果要运行某个文件夹内文件只需在后面跟上相应路径即可。
更多命令请参看 PHPUnit 手册。
== 经验之谈 ==
=== 断言 ===
PHPUnit 提供了各种断言方法 https://phpunit.de/manual/current/zh_cn/appendixes.assertions.html
Lumen 框架也封装了很多断言方法 https://laravel-china.org/docs/5.1/testing ,使用这些断言方法可以很方便的做测试。
PHPUnit 提供的 assertEquals() 是最常用的断言方法,这事因为它的使用非常灵活,可以使用它代替很多断言方法,例如 assertFalse()、assertCount() 等等(例如 assertEquals(false,
=== setUp()函数 ===
setUp() 函数将会在所在测试类中的每个测试前执行,因此,如果测试类中方法较多,不建议在这里写使用 factory 插入数据库的操作,除非这条数据基本每个测试方法都要用到。
tearDown() 函数在每个测试执行完后调用进行清除工作。
在 Lumen 框架中如果不写这两个方法,框架会自动在每个测试执行时自动调用,如果需要修改 setUp() 方法,则务必在自己的 setUp() 中调用 parent::setUp() 方法。
=== factory ===
在 Lumen 中使用 factory 方法插入数据时,一般在 ModelFactory.php 中设置默认填充时,需要将数据表的非空且无默认值项都进行填充,否则会造成插入失败。如果需要修改关键字对应值则在create() 方法中进行修改。
=== 代码覆盖 ===
要完全覆盖接口代码的每一条分支,那么需要为每个 return false 语句对应一个测试用例,当接口涉及的 Model 较多、分支较多时编写测试用例就变得很繁琐,基本需要重新理解每个分支,所以建议在编写接口的过程中编写相应的单元测试,或刚编写完接口后就编写单元测试,这样可以提高效率、并增加代码的质量及时发现 bug ,否则后期添加单元测试将花费较多时间。
另外有些操作,例如 Model 中向数据库插入数据之后判断结果是否返回 false ,只有在数据库异常时才会返回 false ,无法用测试用例进行覆盖。
=== 依赖关系 ===
有的测试方法涉及方法的先后顺序,需要设置依赖关系,具体可参看 PHPUnit 手册的依赖关系一节。 https://phpunit.de/manual/current/zh_cn/writing-tests-for-phpunit.html