phpunit成为单元测试的代名词已成为共识, 但很多在实际编写测试过程中遇到的很多问题通过手册、网上搜索都很难找到相关资料, 大部分都得通过查看源代码和实践的代码经验解决。欢迎大家拍砖。(在此之前请先阅读手册)

测试private/protected方法

类的封装不可避免地会导致private/protected方法的产生,那么如何解决非public的方法?利用反射,使用php提供的相关反射接口可以设置方法的访问权限。

  1. <?php
  2. ...
  3. public function testGetCellphoneByUserId()
  4. {
  5. $userId = 10;
  6. $cellphone = '1333333333';
  7. $method = new \ReflectionMethod('App\UserModel', 'getCellphoneByUserId');
  8. $method = $method->setAccessible(true);
  9. $result = $method->invoke(new App\UserModel(), $userId); // 参数的传递方式为一个参数接着一个参数
  10. $this->assertEquel($cellphone, $result);
  11. }

除此之外, 通过创建一个新类, 继承原类并设置其方法为public也可解决该问题, 但这种解决办法存在很多限制; 如:只能改变protected的方法, 新建文件/类不但麻烦而且降低了可维护性; 相较之下, 通过反射只是代码多了两行,但是一种更好的解决办法.

测试数据库相关代码

phpunit不能执行数据库相关测试, 需安装另一套件完成DbUnit, 安装完成就可使用.按照手册中的四个阶段--建立基境, 执行被测系统, 验证结果, 拆除基境, 可以搭建一个可用测试环境. 在实际项目中, 对数据库的测试就是对模型层的测试, 测试的重点在于数据库方法的(业务的增删改查)封装, 而非ORM. 除了这些概念需要区分开外, 除此之外还有很多问题需要解决。

测试代码的重点

进行数据库的测试是针对数据库方法封装的测试,这是对数据库进行测试的重点。

模型:

  1. public function getUserById($id)
  2. {
  3. return $this->find($id); //orm提供查询单一记录的接口find
  4. }

测试:

  1. // 测试应为对getUserId的测试
  2. public function testGetUserById()
  3. {
  4. ...
  5. $result = $model->getUserById($id);
  6. $this->assertequel($dataSet, $result); // $dataSet为单一记录集
  7. }

建立数据库测试环境(完整示例)

在测试类中要使用 Trait -- PHPUnit_Extensions_Database_TestCase_Trait , 支持命名空间的话为PHPUnit\DbUnit\TestCaseTrait. 该示例中包含了所需的四个阶段.

  1. <?php
  2. namespace Tests;
  3. use PHPUnit_Extensions_Database_TestCase_Trait; // 可选择使用命名空间的写法
  4. use PHPUnit_Framework_TestCase;
  5. use PHPUnit_Extensions_Database_DataSet_ArrayDataSet;
  6. class UserModelTest extends PHPUnit_Framework_TestCase
  7. {
  8. use PHPUnit_Extensions_Database_TestCase_Trait; // 使用DbUnit提供的Trait就可以集成数据库测试功能
  9. /**
  10. * 以数组格式建立数据库基境 (DbUint测试的重要一环)
  11. */
  12. protected function getDateSet()
  13. {
  14. return new PHPUnit_Extensions_Database_DataSet_ArrayDataSet($this->getInitDataSet());
  15. }
  16. /**
  17. * 返回PDO对象 (DbUint测试的重要一环)
  18. */
  19. protected function getConnection()
  20. {
  21. $pdo; // 一般的ORM中基本上都可以获取PDO对象, 直接返回即可
  22. return $this->createDefaultDBConnection($pdo, 'schemaName'); // 可能需要指定库名
  23. }
  24. /**
  25. * 测试方法
  26. */
  27. public function testGetUserById()
  28. {
  29. $user = new User();
  30. $id = $this->getInitDataSet()['user'][0]['id']; // 取值得根据基境数据而来
  31. $result = $user->getUserById($id);
  32. $this->assertEquel($this->getUserByIdDataSet(), $result);
  33. }
  34. /**
  35. * 数据库的初始化数据, 即每次测试之前, 数据库里的数据集就是该基境数据
  36. */
  37. private function getInitDataSet()
  38. {
  39. return [
  40. 'user' => [
  41. [
  42. 'id' => 1,
  43. 'name' => 'joy',
  44. ]
  45. ],
  46. ];
  47. }
  48. /**
  49. * 与通过模型层查询出来的数据进行对比
  50. */
  51. private function getUserByIdDataSet()
  52. {
  53. return $this->getInitDataSet()['user'][0];
  54. }
  55. }

注意:

  1. 不需要setUp 和tearDown 方法

    PHPUnit_Extensions_Database_TestCase_Trait中已有setUp和taerDown方法, 再写则会覆盖trait中的方法, 而且与基境相关的操作都已集成在trait中, 不能被覆盖, 即不能在本类中重写setUp和tearDown.
  2. setUp建立了基境

    查看PHPUnit_Extensions_Database_TestCase_Trait源码, setUp完成了truncate, insert基境数据, 这样的数据库操作就初始化了数据库数据.
  3. tearDown拆除了基境

    查看PHPUnit_Extensions_Database_TestCase_Trait源码, tearDown保持了数据库数据仍是基境数据.

如何建立基境

基境在示例中为数组格式, 格式一定是这样的: 表名做为键值, 表记录以数组表示, 且其中字段名为键名.数组为最方便的. 可以把基境理解成一个存在于内存中的数据库, 只不过查询或更改的数据集是手动代码构建而非数据库执行语句而来.

  1. 一个模型测试中基境只建立一个表数据

    在一个模型中测试多表数据会增加测试的复杂性, 最好是只测试一个模型. 这与代码实现有关, 代码编写需只操作一个数据表.
  2. 对表数据进行断言

    基境为测试的集合,所有进行断言的数据集都需在基境中, 否则容易可能出现数据错误. 示例中:查询的id值在基境数据中获得, 断言集合同样也在基境中获得; 当然, 可以把获取数据的方法进行封装以增强可读性;
  3. 对多表数据进行测试

    上述提到, 只对一个数据表进行测试, 若对多个表进行测试不可避免, 如何解决? 只能建立另一表的基境数据,同时要构建多个模型所需断言的数据集.
  4. 构建基境相关代码会越来越多

    对数据库测试的方法越多, 其需进行断言的数据集越多, 所有这些集合都需代码构建, 构建代码也会越来越多. 面对这种情况唯一能做的就是良好的命名和代码封装, 以达到多但不乱.

如何隔离开发环境

针对模型层的测试会直接对数据库进行增删改查, 所以不可避免的会出现调试/错误数据, 还有基境的构建, 这些属性就决定了数据库的测试不能在除开发环境外的其他任何环境使用. 若开发与测试共用一套数据库实例, 一定要考虑数据紊乱造成的错误, 由于数据错误的排错过程十分头疼。所以针对开发环境与测试的建议是:要么新建一个开发实例, 要么还是不要写数据库相关的测试了。

可以将个问题延伸一下,如何在生产环境使用开发环境的测试?

生产环境与通过测试的代码完全一致,不一样的就是数据。不能直接在生产环境下进行测试, 同时数据也是不能测试的,所以能在测试范围内的操作就是集成测试了。这一点可以通过Gitlab完成,而测试实例只有模拟的http请求即可。

如何测试无返回值的方法

方法中的代码之所以可以被封装成一个方法, 是因为它必然是执行了某一段逻辑, 那这样代码集合必然会改变某些值或状态, 所以测试代码的编写需找出这个变化的值来进行断言. 如:

  1. 删除了某条数据库的记录, 而没有返回值; 查询数据库, 断言无该记录.
  2. 对一个数组元素进行了排序, 而没有返回值; 遍历数据, 断言一个比一个大或一个比一个小.
  3. 更新了缓存; 断言更新前后, 有过期时间则断言过期时间为最新.

对一个类中的所有方法进行上桩

在手册中给出了很多示例, 都单个/具体的方法进行上桩,由此也容易理解桩的概念--改写类中方法的行为. 上桩就是对类进行了一次继承形成一个新对象, 从而改写了方法的行为.但需要对所有方法进行上桩,即屏蔽整个类的操作时,如何处理? 应该对所有方法进行上桩.

  1. $mock = $this->getMockBuilder(\Redis::class)
  2. ->disableOriginalConstructor()
  3. ->getMock();
  4. $mock->expects($this->any()) // 并不限定执行次数
  5. ->method(new \PHPUnit_Framework_Constraint_StringMatches("%a")) // 通过正则完成匹配
  6. ->willReturn(false);

method方法可以接受抽象类PHPUnit_Framework_Constraint和字符串, PHPUnit_Framework_Constraint_StringMatches则是字符串匹配的具体类, 其匹配算法也在该类中.

小结

以上遇到的问题都是的编写代码时遇到的问题,其他问题也还在整理中,也不断遇到新的问题,待问题整理好了,我也会更新上来,欢迎交流拍砖。

参考资料:

开启method访问权限: http://php.net/manual/en/reflectionmethod.setaccessible.php

执行反射method: http://php.net/manual/en/reflectionmethod.invoke.php

phpunit实践笔记的更多相关文章

  1. hadoop2.5.2学习及实践笔记(二)—— 编译源代码及导入源码至eclipse

    生产环境中hadoop一般会选择64位版本,官方下载的hadoop安装包中的native库是32位的,因此运行64位版本时,需要自己编译64位的native库,并替换掉自带native库. 源码包下的 ...

  2. Python编程从入门到实践笔记——异常和存储数据

    Python编程从入门到实践笔记——异常和存储数据 #coding=gbk #Python编程从入门到实践笔记——异常和存储数据 #10.3异常 #Python使用被称为异常的特殊对象来管理程序执行期 ...

  3. Python编程从入门到实践笔记——文件

    Python编程从入门到实践笔记——文件 #coding=gbk #Python编程从入门到实践笔记——文件 #10.1从文件中读取数据 #1.读取整个文件 file_name = 'pi_digit ...

  4. Python编程从入门到实践笔记——类

    Python编程从入门到实践笔记——类 #coding=gbk #Python编程从入门到实践笔记——类 #9.1创建和使用类 #1.创建Dog类 class Dog():#类名首字母大写 " ...

  5. Python编程从入门到实践笔记——函数

    Python编程从入门到实践笔记——函数 #coding=gbk #Python编程从入门到实践笔记——函数 #8.1定义函数 def 函数名(形参): # [缩进]注释+函数体 #1.向函数传递信息 ...

  6. Python编程从入门到实践笔记——用户输入和while循环

    Python编程从入门到实践笔记——用户输入和while循环 #coding=utf-8 #函数input()让程序暂停运行,等待用户输入一些文本.得到用户的输入以后将其存储在一个变量中,方便后续使用 ...

  7. Python编程从入门到实践笔记——字典

    Python编程从入门到实践笔记——字典 #coding=utf-8 #字典--放在{}中的键值对:跟json很像 #键和值之间用:分隔:键值对之间用,分隔 alien_0 = {'color':'g ...

  8. Python编程从入门到实践笔记——if语句

    Python编程从入门到实践笔记——if语句 #coding=utf-8 cars=['bwm','audi','toyota','subaru','maserati'] bicycles = [&q ...

  9. Python编程从入门到实践笔记——操作列表

    Python编程从入门到实践笔记——操作列表 #coding=utf-8 magicians = ['alice','david','carolina'] #遍历整个列表 for magician i ...

随机推荐

  1. JSON 转换异常 multipartRequestHandler servletWrapper

    下面出现红色的字还有警告,解决方法:本人项目是struts1 from类继承了“extends ActionForm” .把它去掉就行了.如果你是其它的框架一定是继承引起的作个参考吧. ....... ...

  2. openvpn配置注意事项

    1.安装VPN安装结束后,需要配置CONFIG文件夹服务端及客户端的配置文件,建议从sample文件里直接拷贝修改,网上的一些案例会引起无法启动的问题,没仔细研究过是哪里错了,反正最后从sample里 ...

  3. 有关苹果无法导出p12证书的问题解决办法。

    原因一 所选类型选择错误.解决办法:左侧有两个分类,一个是钥匙串,一个是种类,要选择种类中的我的证书或者证书.然后在右侧证书列表中,右键导出即可. 原因二 使用钥匙串生成的证书有问题,格式为(cert ...

  4. js中年份、月份下拉框

    <select id="year" style="width: 100px;"></select> <select id=&quo ...

  5. php中有关合并某一字段键值相同的数组合并

    <?php function combine($array,$start,$key,$newkey){ static $new; //静态变量 foreach($array as $k=> ...

  6. Spring Boot快速入门

    安装 安装依赖 maven是一个依赖管理工具,我们利用maven进行构建.创建一个maven项目,在pom.xml里面添加依赖项 <?xml version="1.0" en ...

  7. bootstrap table 插件多语言切换

    在bootstrap中的bootstrap table 插件在多语言切换的审核,只需要如下操作 引入bootstrap-table-locale-all.js文件 $('#Grid').bootstr ...

  8. Docker-compose 多个Docker容器管理:以MYSQL和Wordpress为例

    搬砖的陈大师版权所有,转载请注明:http://www.lenggirl.com/tool/docker-compose.html Docker-compose 多个Docker容器管理:以MYSQL ...

  9. Perl初试

    通过接口发送短信的socket小样: #!/usr/bin/perl -w # auth:lichmama@cnblogs.com # what:send message to phone # usa ...

  10. Java的数据类型和参数传递

    Java提供的数据类型主要分为两大类:基本数据类型和引用数据类型. Java中的基本数据类型 名称 大小 取值范围 byte型 (字节) 8bit -128-127  (-2^7到2^7-1) sho ...