phpunit实践笔记
- 作者:给个理由先
- 本文地址: http://www.cnblogs.com/z1298703836/p/7260883.html
- 转载请注明出处
phpunit成为单元测试的代名词已成为共识, 但很多在实际编写测试过程中遇到的很多问题通过手册、网上搜索都很难找到相关资料, 大部分都得通过查看源代码和实践的代码经验解决。欢迎大家拍砖。(在此之前请先阅读手册)
测试private/protected方法
类的封装不可避免地会导致private/protected方法的产生,那么如何解决非public的方法?利用反射,使用php提供的相关反射接口可以设置方法的访问权限。
<?php
...
public function testGetCellphoneByUserId()
{
$userId = 10;
$cellphone = '1333333333';
$method = new \ReflectionMethod('App\UserModel', 'getCellphoneByUserId');
$method = $method->setAccessible(true);
$result = $method->invoke(new App\UserModel(), $userId); // 参数的传递方式为一个参数接着一个参数
$this->assertEquel($cellphone, $result);
}
除此之外, 通过创建一个新类, 继承原类并设置其方法为public也可解决该问题, 但这种解决办法存在很多限制; 如:只能改变protected的方法, 新建文件/类不但麻烦而且降低了可维护性; 相较之下, 通过反射只是代码多了两行,但是一种更好的解决办法.
测试数据库相关代码
phpunit不能执行数据库相关测试, 需安装另一套件完成DbUnit, 安装完成就可使用.按照手册中的四个阶段--建立基境, 执行被测系统, 验证结果, 拆除基境, 可以搭建一个可用测试环境. 在实际项目中, 对数据库的测试就是对模型层的测试, 测试的重点在于数据库方法的(业务的增删改查)封装, 而非ORM. 除了这些概念需要区分开外, 除此之外还有很多问题需要解决。
测试代码的重点
进行数据库的测试是针对数据库方法封装的测试,这是对数据库进行测试的重点。
模型:
public function getUserById($id)
{
return $this->find($id); //orm提供查询单一记录的接口find
}
测试:
// 测试应为对getUserId的测试
public function testGetUserById()
{
...
$result = $model->getUserById($id);
$this->assertequel($dataSet, $result); // $dataSet为单一记录集
}
建立数据库测试环境(完整示例)
在测试类中要使用 Trait -- PHPUnit_Extensions_Database_TestCase_Trait
, 支持命名空间的话为PHPUnit\DbUnit\TestCaseTrait
. 该示例中包含了所需的四个阶段.
<?php
namespace Tests;
use PHPUnit_Extensions_Database_TestCase_Trait; // 可选择使用命名空间的写法
use PHPUnit_Framework_TestCase;
use PHPUnit_Extensions_Database_DataSet_ArrayDataSet;
class UserModelTest extends PHPUnit_Framework_TestCase
{
use PHPUnit_Extensions_Database_TestCase_Trait; // 使用DbUnit提供的Trait就可以集成数据库测试功能
/**
* 以数组格式建立数据库基境 (DbUint测试的重要一环)
*/
protected function getDateSet()
{
return new PHPUnit_Extensions_Database_DataSet_ArrayDataSet($this->getInitDataSet());
}
/**
* 返回PDO对象 (DbUint测试的重要一环)
*/
protected function getConnection()
{
$pdo; // 一般的ORM中基本上都可以获取PDO对象, 直接返回即可
return $this->createDefaultDBConnection($pdo, 'schemaName'); // 可能需要指定库名
}
/**
* 测试方法
*/
public function testGetUserById()
{
$user = new User();
$id = $this->getInitDataSet()['user'][0]['id']; // 取值得根据基境数据而来
$result = $user->getUserById($id);
$this->assertEquel($this->getUserByIdDataSet(), $result);
}
/**
* 数据库的初始化数据, 即每次测试之前, 数据库里的数据集就是该基境数据
*/
private function getInitDataSet()
{
return [
'user' => [
[
'id' => 1,
'name' => 'joy',
]
],
];
}
/**
* 与通过模型层查询出来的数据进行对比
*/
private function getUserByIdDataSet()
{
return $this->getInitDataSet()['user'][0];
}
}
注意:
- 不需要setUp 和tearDown 方法
在PHPUnit_Extensions_Database_TestCase_Trait
中已有setUp和taerDown方法, 再写则会覆盖trait中的方法, 而且与基境相关的操作都已集成在trait中, 不能被覆盖, 即不能在本类中重写setUp和tearDown. - setUp建立了基境
查看PHPUnit_Extensions_Database_TestCase_Trait
源码, setUp完成了truncate, insert基境数据, 这样的数据库操作就初始化了数据库数据. - tearDown拆除了基境
查看PHPUnit_Extensions_Database_TestCase_Trait
源码, tearDown保持了数据库数据仍是基境数据.
如何建立基境
基境在示例中为数组格式, 格式一定是这样的: 表名做为键值, 表记录以数组表示, 且其中字段名为键名.数组为最方便的. 可以把基境理解成一个存在于内存中的数据库, 只不过查询或更改的数据集是手动代码构建而非数据库执行语句而来.
- 一个模型测试中基境只建立一个表数据
在一个模型中测试多表数据会增加测试的复杂性, 最好是只测试一个模型. 这与代码实现有关, 代码编写需只操作一个数据表. - 对表数据进行断言
基境为测试的集合,所有进行断言的数据集都需在基境中, 否则容易可能出现数据错误. 示例中:查询的id值在基境数据中获得, 断言集合同样也在基境中获得; 当然, 可以把获取数据的方法进行封装以增强可读性; - 对多表数据进行测试
上述提到, 只对一个数据表进行测试, 若对多个表进行测试不可避免, 如何解决? 只能建立另一表的基境数据,同时要构建多个模型所需断言的数据集. - 构建基境相关代码会越来越多
对数据库测试的方法越多, 其需进行断言的数据集越多, 所有这些集合都需代码构建, 构建代码也会越来越多. 面对这种情况唯一能做的就是良好的命名和代码封装, 以达到多但不乱.
如何隔离开发环境
针对模型层的测试会直接对数据库进行增删改查, 所以不可避免的会出现调试/错误数据, 还有基境的构建, 这些属性就决定了数据库的测试不能在除开发环境外的其他任何环境使用. 若开发与测试共用一套数据库实例, 一定要考虑数据紊乱造成的错误, 由于数据错误的排错过程十分头疼。所以针对开发环境与测试的建议是:要么新建一个开发实例, 要么还是不要写数据库相关的测试了。
可以将个问题延伸一下,如何在生产环境使用开发环境的测试?
生产环境与通过测试的代码完全一致,不一样的就是数据。不能直接在生产环境下进行测试, 同时数据也是不能测试的,所以能在测试范围内的操作就是集成测试了。这一点可以通过Gitlab完成,而测试实例只有模拟的http请求即可。
如何测试无返回值的方法
方法中的代码之所以可以被封装成一个方法, 是因为它必然是执行了某一段逻辑, 那这样代码集合必然会改变某些值或状态, 所以测试代码的编写需找出这个变化的值来进行断言. 如:
- 删除了某条数据库的记录, 而没有返回值; 查询数据库, 断言无该记录.
- 对一个数组元素进行了排序, 而没有返回值; 遍历数据, 断言一个比一个大或一个比一个小.
- 更新了缓存; 断言更新前后, 有过期时间则断言过期时间为最新.
对一个类中的所有方法进行上桩
在手册中给出了很多示例, 都单个/具体的方法进行上桩,由此也容易理解桩的概念--改写类中方法的行为. 上桩就是对类进行了一次继承形成一个新对象, 从而改写了方法的行为.但需要对所有方法进行上桩,即屏蔽整个类的操作时,如何处理? 应该对所有方法进行上桩.
$mock = $this->getMockBuilder(\Redis::class)
->disableOriginalConstructor()
->getMock();
$mock->expects($this->any()) // 并不限定执行次数
->method(new \PHPUnit_Framework_Constraint_StringMatches("%a")) // 通过正则完成匹配
->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实践笔记的更多相关文章
- hadoop2.5.2学习及实践笔记(二)—— 编译源代码及导入源码至eclipse
生产环境中hadoop一般会选择64位版本,官方下载的hadoop安装包中的native库是32位的,因此运行64位版本时,需要自己编译64位的native库,并替换掉自带native库. 源码包下的 ...
- Python编程从入门到实践笔记——异常和存储数据
Python编程从入门到实践笔记——异常和存储数据 #coding=gbk #Python编程从入门到实践笔记——异常和存储数据 #10.3异常 #Python使用被称为异常的特殊对象来管理程序执行期 ...
- Python编程从入门到实践笔记——文件
Python编程从入门到实践笔记——文件 #coding=gbk #Python编程从入门到实践笔记——文件 #10.1从文件中读取数据 #1.读取整个文件 file_name = 'pi_digit ...
- Python编程从入门到实践笔记——类
Python编程从入门到实践笔记——类 #coding=gbk #Python编程从入门到实践笔记——类 #9.1创建和使用类 #1.创建Dog类 class Dog():#类名首字母大写 " ...
- Python编程从入门到实践笔记——函数
Python编程从入门到实践笔记——函数 #coding=gbk #Python编程从入门到实践笔记——函数 #8.1定义函数 def 函数名(形参): # [缩进]注释+函数体 #1.向函数传递信息 ...
- Python编程从入门到实践笔记——用户输入和while循环
Python编程从入门到实践笔记——用户输入和while循环 #coding=utf-8 #函数input()让程序暂停运行,等待用户输入一些文本.得到用户的输入以后将其存储在一个变量中,方便后续使用 ...
- Python编程从入门到实践笔记——字典
Python编程从入门到实践笔记——字典 #coding=utf-8 #字典--放在{}中的键值对:跟json很像 #键和值之间用:分隔:键值对之间用,分隔 alien_0 = {'color':'g ...
- Python编程从入门到实践笔记——if语句
Python编程从入门到实践笔记——if语句 #coding=utf-8 cars=['bwm','audi','toyota','subaru','maserati'] bicycles = [&q ...
- Python编程从入门到实践笔记——操作列表
Python编程从入门到实践笔记——操作列表 #coding=utf-8 magicians = ['alice','david','carolina'] #遍历整个列表 for magician i ...
随机推荐
- textarea的中文输入判断与搜狗输入法的特殊行为
虽然要讲解的知识点是通用的,但是还是要介绍下我的应用场景和测试环境. 0.1 应用场景和测试环境 我的应用是一块使用Html Canvas开发的黑板,在黑板上实现简单的文字编辑功能. 操作系统:win ...
- 微信小程序开发过程中一些经验总结
1.微信开发者工具报错,微信小程序最低需支持tls1.2版本的问题 原因是服务器不支持ssl的高版本,解决方法: 在/etc/nginx/conf.d文件下,把"ssl_protocols ...
- R语言统计分析技术研究——岭回归技术的原理和应用
岭回归技术的原理和应用 作者马文敏 岭回归分析是一种专用于共线性分析的有偏估计回归方法,实质上是一种改良的最小二乘估计法,通过放弃最小二乘法的无偏性,以损失部分信息,降低精度为代价获得回归系数更为符合 ...
- swiper结合ajax的轮播图
Swiper 是什么:是纯JavaScript打造的滑动特效插件,能够实现触屏焦点图.触屏tab切换.触屏多图切换等常用效果. 开源.免费.稳定.应用广泛. 这就是swiper简单的介绍,由于是结合a ...
- MVC过滤器之添加LoginAttribute,浏览器bug:重定向次数太多
以前在写登录Action过滤时,都在每个Controller前写上CheckLoginAttribute:这次决定偷懒试一下能否将所有Action和Controller统一过滤: 出bug的代码是这样 ...
- Solr6.6 Tomcat8部署
原文:https://github.com/x113773/testall/issues/6 准备工作:[solr-6.6.0](http://www.apache.org/dyn/closer.lu ...
- [leetcode-515-Find Largest Value in Each Tree Row]
You need to find the largest value in each row of a binary tree. Example: Input: 1 / \ 3 2 / \ ...
- eclipse在多modules项目结构下避免模块间依赖引用的场景
这个在单一classLoader时,不会有问题.如果多classloader下会有问题. 假设工程有两个模块,module2 依赖module1 当我们执行mvc eclipse:eclipse后,然 ...
- AngularJs + angular-ui-router + bootstrap 实现blog基础导航功能
AngularJs + angular-ui-router + bootstrap 实现blog基础导航功能 核心代码如下 1.index.html <!DOCTYPE html> < ...
- 通过线程监控socket服务器是否done机
现实中的socket可能会因为各种原因done机,但这么重要的服务器怎么能允许这种事情发生?这次我们就来通过一个线程去监控socket服务器,如果done机重新将其启动. 下面是监控项目和socket ...