php单元测试(phpunit)中自定义万能通用仿件
一、背景
如果在项目中经常用phpunit来做单元测试的话(所以看此文章的伙伴们需要单元测试基础),应该都知道,最重要的是依赖的模拟,也就是仿件或者打桩,所以你一定遇到过各种情况的依赖模拟困难,最近我就遇到一个大部分人或者代码中都会出现的模拟依赖困难的情况。这也是本文中通用讲到的一个例子,场景如下:
其中我们只需测试UserService类,UserService类写成代码为:
<?php
namespace app\service\tanjiajun;
use app\lib\App;
use app\model\tanjiajun\UserModel;
class UserService
{
public function getUserOrderList()
{
$userModel = App::make(UserModel::class);
$userList = $userModel::find()
->select()
->asArray()
->all();
$result = [];
foreach ($userList as $user) {
$result[$user['uid']] = $userModel->getOrdersByUid($user['uid']);
}
return $result;
}
}
其中App::make()方法是框架中的方法,在IOC容器中取出一个对象,相当于new UserModel()。
二、继承版Mock仿件
看完UserService类后,我们知道它依赖了UserModel的各种查询方法,有一些Model自带的方法,如select、all和自定义的方法getOrdersByUid()。要测试这个UserService类,我们必须把UserModel的这些方法给模拟掉才行,因为我们不能让UserModel的变化而影响结果的断言。要怎么模拟呢?如果用phpunit本身自带的桩件和Mock是做不到的,除了这个,一般采用一种匿名类继承被模拟类,然后覆盖父类(也就是被模拟类)的一些方法。所以,我们可以这么来写UserModel的Mock,下面是测试类的测试方法:
<?php
namespace tests;
use app\lib\App;
use app\service\tanjiajun\UserService;
use app\model\tanjiajun\UserModel;
class UserServiceTest extends TestCase
{
/**
* @test
*/
public function getUserList()
{
/*创建UserModel的仿件,继承被模拟类方式*/
$userModelMock = new class extends UserModel
{
private static $returnMap = [];
public static function find()
{
return self::$returnMap['find'] ?: null;
}
public function slave()
{
return self::$returnMap['slave'] ?: null;
}
public function select()
{
return self::$returnMap['select'] ?: null;
}
public function asArray()
{
return self::$returnMap['asArray'] ?: null;
}
public function all()
{
return self::$returnMap['all'] ?: null;
}
public function getOrdersByUid($uid)
{
return self::$returnMap['getOrdersByUid'][$uid] ?: null;
}
public function setReturnMap($map)
{
self::$returnMap = $map;
}
};
$map = [
'find' => $userModelMock,
'select' => $userModelMock,
'slave' => $userModelMock,
'asArray' => $userModelMock,
'all' => [
['uid' => 1, 'name' => 'jack'],
['uid' => 2, 'name' => 'tom'],
],
'getOrdersByUid' => [
'1' => ['uid' => 1, 'order_id' => 1, 'name' => 'jack'],
'2' => ['uid' => 2, 'order_id' => 2, 'name' => 'tom'],
]
];
$userModelMock->setReturnMap($map);//设置仿件返回值
App::getContainer()->instance(UserModel::class, $userModelMock);//替换IOC容器中的UserModel
//调用被测类UserService
$userService = App::make(UserService::class);
$ret = $userService->getUserOrderList();
//断言结果
$this->assertEquals([
'1' => ['uid' => 1, 'order_id' => 1, 'name' => 'jack'],
'2' => ['uid' => 2, 'order_id' => 2, 'name' => 'tom'],
], $ret);
}
}
这种方法是同个继承UserModel类,然后重写掉在UserService调用的一些方法,然后通过一个指定map变量,返回我们期望的值。这种方法的好处是:
1、完全基于UserModel的特性环境去改造返回
2、实现比较简单
而坏处是:
1、建立的仿件userModelMock代码量太多
2、重复的复写了很多返回一样的配置,如select、find这些方法
3、无法复用,只能是在特定的方法中使用,如果下一个被测service还是用到这些方法,还得写一次同样代码
三、通用的万能仿件SupperMock
基于上面的实现方式带来的缺点,我们是不是可以改装一下,把仅限于UserModel的Mock改成通过的方法或者类去生成呢?要通过,必须解决这几点:
1、像model这种本身已经具有的基础方法,像select、where、find等,很多时候都是用来做连贯操作查询的,我们统统默认返回$this,也就是当前类。怎么实现呢,我们这里用了一个小技巧,也是实现万能仿件的关键,就是魔术方法__call()和__callStatic()。这次我们的匿名类不用继承被仿类,直接当调用者调用到不存在的方法,如select、where等时,默认返回$this。而当调用到需求返回特定结果的方法时,读预先配置好的返回Map数组,返回指定的结果即可。这样达到的效果就是动态的生成了类中的方法,这也是我们这个仿件中非常关键的特性。
2、对于方法输入不同的参数,返回不同值的配置Map又怎么去实现?这里我们直接用方法名+参数做数据Map的key,但是参数可能是数组,所成生产唯一key的方法变成MD5(方法名+json_encode(输入参数数组))。
所有问题都解决后,大致关系流程总结如下:
代码实现为
单元测试类UserService.php(仿件调用方)
<?php
namespace tests;
use app\lib\App;
use app\service\tanjiajun\UserService;
use app\model\tanjiajun\UserModel;
class UserServiceTest extends TestCase
{
/**
* @test
*/
public function getUserList()
{
/*创建UserModel的仿件*/
$userModelMock = $this->createSuperMock(UserModel::class);
/*设置普通方法返回的Map*/
$methodMap = [
'all' => array(
array('return' => [['uid' => 1, 'name' => 'jack'], ['uid' => 2, 'name' => 'tom']])
),
'getOrdersByUid' => array(
/*args为方法输入参数,return是对应返回值,args为null的话默认返回当前类$this*/
array('return' => ['uid' => 1, 'order_id' => 1, 'name' => 'jack'], 'args' => [1]),
array('return' => ['uid' => 2, 'order_id' => 2, 'name' => 'tom'], 'args' => [2]),
),
];
$userModelMock->willReturn($methodMap);
/*设置静态方法返回的Map*/
$staticMethodMap = [
'find' => array(
array('return' => $userModelMock)
)
];
$userModelMock::staticWillReturn($staticMethodMap);
App::getContainer()->instance(UserModel::class, $userModelMock);//替换IOC容器中的UserModel
//调用被测类UserService
$userService = App::make(UserService::class);
$ret = $userService->getUserOrderList();
//断言结果
$this->assertEquals([
'1' => ['uid' => 1, 'order_id' => 1, 'name' => 'jack'],
'2' => ['uid' => 2, 'order_id' => 2, 'name' => 'tom'],
], $ret);
}
}
创建SupperMock::class的统一方法,我放在了TestCase.php下
TestCase.php
<?php
namespace tests;
use app\lib\App;
use tests\mock\SupperMock;
class TestCase extends \PHPUnit\Framework\TestCase
{
/**
* 创建超级仿件
* @param String $className
* @return mixed
*/
public function createSuperMock(String $className)
{
return App::makeWith(SupperMock::class, ['className' => $className]);
}
}
最后是这个万能仿件SupperMock.php
<?php
/**
* 超级仿件
* User: TanJiaJun
* Date: 2018/11/10
* Time: 14:25
*/
namespace tests\mock;
class SupperMock
{
private $methodReturnMap;
protected $mockClass;
protected static $mockClassName;
protected static $mockClassMethod;
protected $mockClassStaticMethod;
public static $staticMethodReturnMap = [];
public function __construct($className)
{
self::$mockClassName = $className;
}
/**普通方法返回处理
* @param $name
* @param $arguments
* @return SupperMock
*/
function __call($name, $arguments)
{
$mapKey = $this->generateMapKey($name, $arguments);
return $this->methodReturnMap[$mapKey] ?: $this;
}
/**静态方法返回处理
* @param $name
* @param $arguments
* @return SupperMock
*/
function __callStatic($name, $arguments)
{
$mapKey = self::generateMapKey($name, $arguments);
return self::$staticMethodReturnMap[$mapKey] ?: new self(self::$mockClassName);
}
/**
* 设置普通方法返回Map
* @param $willReturn
*/
public function willReturn($willReturn)
{
foreach ($willReturn as $method => $methodMap) {
foreach ($methodMap as $val) {
$mapKey = $this->generateMapKey($method, $val['args']);
$this->methodReturnMap[$mapKey] = $val['return'];
}
}
}
/**设置静态方法返回Map
* @param $willReturn
*/
public static function staticWillReturn($willReturn)
{
foreach ($willReturn as $method => $methodMap) {
foreach ($methodMap as $val) {
$mapKey = self::generateMapKey($method, $val['args']);
self::$staticMethodReturnMap[$mapKey] = $val['return'];
}
}
}
/**
* 生产MapKey:MD5(方法名+json_encode(参数))
* @param $method
* @param $args
* @return string
*/
private static function generateMapKey($method, $args)
{
if (empty($args)) {
return md5($method);
}
return md5($method . json_encode($args));
}
}
测试结果:
四、其他场景应用
例如service中依赖了cache之类的
service类
<?php
namespace app\service\tanjiajun;
use app\lib\App;
class CommonService
{
public function testMc($key = "")
{
$cache = App::getCache();
$mc = $cache::getMemcached();
return $mc->get($key);
}
}
依赖的缓存工具类
class Cache {
public static function getMemcached($server_id = 2) {
$cacheKey = __METHOD__ . '-' . $server_id;
return Process::staticCache($cacheKey, function() use ($server_id) {
$serverInfo = get_memcache_config_array()[$server_id] ?? null;
if (empty($serverInfo)) {
throw new ConfigException('Memcached缓存配置不存在');
}
if (is_array($serverInfo)) {
$host = $serverInfo['host'];
$port = $serverInfo['port'];
$user = $serverInfo['user'];
$pwd = $serverInfo['pwd'];
} else {
list($host, $port) = explode(':', $serverInfo);
}
$memcached = new Memcached();
$memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true); //使用binary二进制协议
$memcached->addServer($host, $port); //添加实例地址 端口号
if(!empty($user)) {
$memcached->setSaslAuthData($user, $pwd); //设置OCS帐号密码进行鉴权
}
return $memcached;
});
}
}
测试类
<?php
namespace tests;
use app\lib\App;
use app\service\tanjiajun\CommonService;
use infra\tool\Cache;
class CommonServiceTest extends TestCase
{
/**
* @test
*/
public function testMc()
{
/*创建MemcacheMock*/
$mcMock = $this->createSuperMock("Memcache");
$mcMap = [
'get' => array(
array('return' => 'key1_result', 'args' => ['key1']),
array('return' => 'key2_result', 'args' => ['key2']),
),
];
$mcMock->willReturn($mcMap);
/*创建CacheMock*/
$cacheMock = $this->createSuperMock(Cache::class);
$staticMethodMap = [
'getMemcached' => array(
array('return' => $mcMock)
)
];
$cacheMock::staticWillReturn($staticMethodMap);
App::getContainer()->instance(Cache::class, $cacheMock);//替换IOC容器中的Cache
$testObj = App::make(CommonService::class);
$ret = $testObj->testMc('key1');
$this->assertEquals('key1_result', $ret);
}
}

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
打通前后端构建一个Vue+Express的开发环境
前端做多了,自然就会想往后端伸手。更何况现在有了Node和Express,让前端做后端开发的学习降低了不少。 最近正好揽下了一个 Vue + Express + mysql 的小项目。项目开始时是前后端分开开发的,但是当后台做好一部分要进行连接测试时问题就来了。前端部分在每次修改过就需要build一次,而后台也每次需要重启一次服务器才能进行调试。可想而知,这样的开发和调试效率是很低的。在这么煎熬着做了一个功能之后,实在受不了。于是便想有没有什么方法可以热更新开发前后端。毕竟 vue-cli 在调试时,后台就是用 express 做的服务器。 在网上搜索了一下,发现果然有前辈们已经想到这个问题,并做了一个 Vue + Express 的开发环境。 可以参考这篇文章:从零开始搭建 Express + Vue 开发环境 原理的话可以参考这篇文章,主要就是通过修改 webpack 的配置文件来达到同时开发前后台的目的。 改造后的版本: 改造后目录 其中关于 webpack 的配置,没有使用原项目的配置文件,而是几乎将 vue-cli 生成的项目的配置文件都拷过来使用的。 这里需要注意的是,下面...
-
下一篇
webpack4配置详解之逐行逐句分析
前言 经常会有群友问起webpack、react、redux、甚至create-react-app配置等等方面的问题,有些是我也不懂的,慢慢从大家的相互交流中,也学到了不少。 今天就尝试着一起来聊聊Webpack吧,旨在帮大家加深理解、新手更容易上路,都能从0到1搭建配置自定属于自己的脚手架,或对已封装好的脚手架有进一步的巩固,接下来苏南会详细讲解webpack中的每一个配置字段的作用(部分为webpack4新增)。 近两年,前端一直在以一个高速持续的过程发展,常常会有网友在调侃老了、学不动了, 虽是在调侃却又间接阐述着无奈,迫于生活的压力,不得不提速前行, 因为没有谁为你而停留,公司不会、社会不会、同伴不会……,停下可能将意味着淘汰 —— 理想很丰满,现实很骨感,所以让我们一起进步,共同加薪,奋斗吧骚年,加油。。 ~~吐槽过了,接着聊正事~~。 原谅我控制不住自己,想问下各位,昨天刚刚过去的双十一你脱单了吗? 人生若只如初见,何事秋风悲画扇; 等闲变却故人心,却道故人心易变; 骊山语罢清宵半,夜雨霖铃终不怨。 各位大佬早安,这里是@IT·平头哥联盟,我是首席填坑官∙苏南,用心分享 ...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Hadoop3单机部署,实现最简伪集群
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS关闭SELinux安全模块
- CentOS6,CentOS7官方镜像安装Oracle11G
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- Crontab安装和使用
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS7,CentOS8安装Elasticsearch6.8.6