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

1、背景

若是在项目中常常用phpunit来作单元测试的话(因此看此文章的伙伴们须要单元测试基础),应该都知道,最重要的是依赖的模拟,也就是仿件或者打桩,因此你必定遇到过各类状况的依赖模拟困难,最近我就遇到一个大部分人或者代码中都会出现的模拟依赖困难的状况。这也是本文中通用讲到的一个例子,场景以下:php

 

 

 

 

 

 

 

 

 

 

 

 

 

其中咱们只需测试UserService类,UserService类写成代码为:json

<?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()。数组

2、继承版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变量,返回咱们指望的值。这种方法的好处是:app

一、彻底基于UserModel的特性环境去改造返回框架

二、实现比较简单memcached

而坏处是:工具

一、创建的仿件userModelMock代码量太多单元测试

二、重复的复写了不少返回同样的配置,如select、find这些方法测试

三、没法复用,只能是在特定的方法中使用,若是下一个被测service仍是用到这些方法,还得写一次一样代码

3、通用的万能仿件SupperMock

基于上面的实现方式带来的缺点,咱们是否是能够改装一下,把仅限于UserModel的Mock改为经过的方法或者类去生成呢?要经过,必须解决这几点:

一、像model这种自己已经具备的基础方法,像select、where、find等,不少时候都是用来作连贯操做查询的,咱们通通默认返回$this,也就是当前类。怎么实现呢,咱们这里用了一个小技巧,也是实现万能仿件的关键,就是魔术方法__call()和__callStatic()。此次咱们的匿名类不用继承被仿类,直接当调用者调用到不存在的方法,如select、where等时,默认返回$this。而当调用到需求返回特定结果的方法时,读预先配置好的返回Map数组,返回指定的结果便可。这样达到的效果就是动态的生成了类中的方法,这也是咱们这个仿件中很是关键的特性。

二、对于方法输入不一样的参数,返回不一样值的配置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));
    }
}

测试结果:

4、其余场景应用

例如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);
    }
}
相关文章
相关标签/搜索