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];
}
}

注意:

  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. 更新了缓存; 断言更新前后, 有过期时间则断言过期时间为最新.

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

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

$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实践笔记的更多相关文章

  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. gawk的用法

        GNU gawk工具的功能是将指定文件中符合指定模式(pattern)的行按指定的动作(action)进行格式化处理 语法:gawk [options] [program] [file-lis ...

  2. Linux回炉复习系列文章大纲

    本人最近在回炉Linux的内容,也做了很多整理,顺便也想将整理的内容分享出来. 由于该系列文章的内容主要是复习整理而来,其中绝大多数命令都是翻译和整理man或info文档总结的,另外很多地方也没有给出 ...

  3. 【CC2530入门教程-01】IAR集成开发环境的建立与项目开发流程

    [引言] 本系列教程就有关CC2530单片机应用入门基础的实训案例进行分析,主要包括以下6部分的内容:1.CC2530单片机开发入门.2.通用I/O端口的输入和输出.3.外部中断初步应用.4.定时/计 ...

  4. document事件及例子

    一.关于鼠标事件:onclick:鼠标单击触发 ondbclick:鼠标双击触发 onmouseover:鼠标移上触发 onmouseout:鼠标离开触发 onmousemove:鼠标移动触发 二.关 ...

  5. Akka(8): 分布式运算:Remoting-远程查找式

    Akka是一种消息驱动运算模式,它实现跨JVM程序运算的方式是通过能跨JVM的消息系统来调动分布在不同JVM上ActorSystem中的Actor进行运算,前题是Akka的地址系统可以支持跨JVM定位 ...

  6. vue组件大集合 component

    vue组件分为全局组件.局部组件和父子组件,其中局部组件只能在el定义的范围内使用, 全局组件可以在随意地方使用,父子组件之间的传值问题等. Vue.extend 创建一个组件构造器 template ...

  7. jQuery 评分插件(转)

    评分效果的小插件jQuery Raty.它提供的API相当丰富真的是让人爱不释手.详细文档及下载插件请移步这里. 基本使用 下面我们来实际操作,运用一下这个有爱的小插件. 需要做的事情非常简单,在页面 ...

  8. ASP.NET Core Web 资源打包与压缩

    本文将介绍使用的打包和压缩的优点,以及如何在ASP.NET Core应用程序中使用这些功能. 概述 在ASP.Net中可以使用打包与压缩这两种技术来提高Web应用程序页面加载的性能.通过减少从服务器请 ...

  9. Vijos 1006 晴天小猪历险记之Hill 单源单汇最短路

    背景 在很久很久以前,有一个动物村庄,那里是猪的乐园(^_^),村民们勤劳.勇敢.善良.团结-- 不过有一天,最小的小小猪生病了,而这种病是极其罕见的,因此大家都没有储存这种药物.所以晴天小猪自告奋勇 ...

  10. 基于Bootstrap+angular的一个豆瓣电影app

    1.搭建项目框架 npm初始化项目 npm init -y //按默认配置初始化项目 安装需要的第三方库 npm install bootstrap angular angular-route --s ...