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. Jquery取属性值(复选框、下拉列表、单选按钮)、做全选按钮、JSON存储、去空格

    1.jquery取复选框的值 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "htt ...

  2. WKWebView 官方文档

    WKWebView 类 一个WKWebView对象可以显示交互式的web内容.就像一个应用程序的浏览器.你可以使用WKWebView类嵌入Web内容的应用程序.这样做,创造一个WKWebView对象, ...

  3. log4j 在项目中的详细配置

    1.添加log4j 包 2.首先在src目录下添加log4j.properties文件 log4j.rootLogger=debug, stdout, R log4j.appender.stdout= ...

  4. 在PHP中连接数据库的八大步骤

                                                                                   连接数据库的八大步骤  Step1:链接数 ...

  5. 为Windows资源管理器右键菜单添加菜单项

    为Windows资源管理器右键菜单添加菜单项 在Windows下命令行用的比较多,经常在资源管理器里翻到某个目录,若想要在此目录下跑命令,只能是打开cmd.exe,然后一路cd才能到达此目录. 每次都 ...

  6. 【ASP.NET MVC 牛刀小试】 URL Route

    例子引入 先看看如下例子,你能完全明白吗? using System; using System.Collections.Generic; using System.Linq; using Syste ...

  7. 【Android Developers Training】 96. 运行一个同步适配器

    注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...

  8. 经验之谈——gulp使用教程

    gulp的最实用教程 使用gulp编译less.sass.压缩js等常用功能讲解 gulp是前端开发过程中对代码进行构建的工具,是自动化项目的构建利器:她不仅能对网站资源进行优化,而且在开发过程中很多 ...

  9. 为实体类增加toJSON方法

    后期子类继承该基础类即可. package com.lichmama.test; import java.io.Serializable; import java.lang.reflect.Field ...

  10. 第三章:3.3 post 请求

    1. 在 from表单中将 属性 methtod="post‘ 改变成post 2. 访问主页地址:http://localhost:8000/index 3. 以上出现的错误. 查资料发现 ...