若是在項目中常常用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()。數組
看完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仍是用到這些方法,還得寫一次一樣代碼
基於上面的實現方式帶來的缺點,咱們是否是能夠改裝一下,把僅限於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)); } }
測試結果:
例如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); } }