文档章节

php单元测试(phpunit)中自定义万能通用仿件

tanjj
 tanjj
发布于 2018/11/11 22:44
字数 1944
阅读 222
收藏 2

一、背景

如果在项目中经常用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);
    }
}

 

© 著作权归作者所有

tanjj
粉丝 18
博文 28
码字总数 29300
作品 0
广州
程序员
私信 提问
Zend Studio使用教程:使用PHPUnit检测代码(一)

【特惠专享】Zend Guard在线订购专享特别优惠!在线订购>> 【特惠专享】Zend Studio线订购专享特别优惠!在线订购>> 本教程演示如何在代码上创建和运行PHPUnit Test。您将学习如何创建和运行...

电池盒
01/10
16
0
使用 PHPUnit 和 Selenium 进行测试

文章出处:http://netbeans.org/kb/docs/php/phpunit_zh_CN.html 适用于 PHP 的 NetBeans IDE 支持 PHPUnit 自动测试。通过 PHPUnit,NetBeans IDE 可为 PHP 提供代码覆盖率,这与 IDE 为 Py...

红薯
2011/12/02
5.5K
4
在netbeans中无法显示单元覆盖率(XAMPP+Yii+PHPUnit)

本地开发套件:XAMPP IDE:NetBeans 7.3 Beta2 PHP框架:Yii 单元测试工具:PHPUnit 3.7.21 测试用例的写法不是继承PHPUnit的那几个类,而是使用Yii里面的那套单元测试类。 命令行调用PHPUn...

茶包
2013/06/27
484
0
Zend Studio使用教程:使用PHPUnit检测代码(二)

【特惠专享】Zend Guard在线订购专享特别优惠!在线订购>> 【特惠专享】Zend Studio线订购专享特别优惠!在线订购>> 本教程演示如何在代码上创建和运行PHPUnit Test。您将学习如何创建和运行...

电池盒
01/11
12
0
Zend Studio使用教程:使用PHPUnit检测代码(五)

本教程演示如何在代码上创建和运行PHPUnit Test。您将学习如何创建和运行包含许多测试用例的单个单元测试用例和测试套件。 单元测试是一个测试代码的过程,以确保源代码的各个单元正常工作,...

电池盒
01/16
7
0

没有更多内容

加载失败,请刷新页面

加载更多

500行代码,教你用python写个微信飞机大战

这几天在重温微信小游戏的飞机大战,玩着玩着就在思考人生了,这飞机大战怎么就可以做的那么好,操作简单,简单上手。 帮助蹲厕族、YP族、饭圈女孩在无聊之余可以有一样东西让他们振作起来!...

上海小胖
20分钟前
1
0
关于AsyncTask的onPostExcute方法是否会在Activity重建过程中调用的问题

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://blog.csdn.net/XG1057415595/article/details/86774575 假设下面一种情况...

shzwork
今天
7
0
object 类中有哪些方法?

getClass(): 获取运行时类的对象 equals():判断其他对象是否与此对象相等 hashcode():返回该对象的哈希码值 toString():返回该对象的字符串表示 clone(): 创建并返此对象的一个副本 wait...

happywe
今天
6
0
Docker容器实战(七) - 容器中进程视野下的文件系统

前两文中,讲了Linux容器最基础的两种技术 Namespace 作用是“隔离”,它让应用进程只能看到该Namespace内的“世界” Cgroups 作用是“限制”,它给这个“世界”围上了一圈看不见的墙 这么一...

JavaEdge
今天
8
0
文件访问和共享的方法介绍

在上一篇文章中,你了解到文件有三个不同的权限集。拥有该文件的用户有一个集合,拥有该文件的组的成员有一个集合,然后最终一个集合适用于其他所有人。在长列表(ls -l)中这些权限使用符号...

老孟的Linux私房菜
今天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部