如何在PHPUnit中模拟单例类方法并返回静态值?
问题:如何模拟单例依赖类的方法进行PHPUnit测试?
我正在为依赖其他类的类编写测试用例,希望模拟被依赖类的方法并返回静态数据。尝试为CacheManager类编写PHPUnit测试,该类依赖于RestApiHandler单例类,但测试时出现错误。
相关代码
CacheManager类
<?php // -*- coding: utf-8 -*- declare(strict_types=1); namespace Sayem\Users; class CacheManager { use Traits\Singleton; private $usersCacheKey = 'au_users_data'; private $userCacheKey = 'au_user_data'; private $settings; public function __construct() { $this->settings = ['cache-lifespan' => 6]; } public function usersCache(): array { $refreshData = false; $cachedData = get_transient($this->usersCacheKey); if (false === $cachedData) { // Cache doesn't exist or has expired, so generate new data and set cache $refreshData = true; // phpcs:ignore Inpsyde.CodeQuality.NoElse.ElseFound } else { $data = unserialize($cachedData); if (isset($data['error']) && $data['error']) { // Error cache, so generate new data and set cache $refreshData = true; } } if ($refreshData) { $allUsers = RestApiHandler::instance()->allUsersApiRequest(); set_transient( $this->usersCacheKey, serialize($allUsers), $this->hoursInSecond((int)$this->settings['cache-lifespan']) ); return $allUsers; } return unserialize($cachedData); } }
RestApiHandler类
<?php // -*- coding: utf-8 -*- declare(strict_types=1); namespace Sayem\Users; class RestApiHandler { use Traits\Singleton; public function allUsersApiRequest(): array { $url = 'https://jsonplaceholder.typicode.com/users'; $response = \wp_remote_get($url); if (is_wp_error($response)) { // Handle the error here $errorMessage = $response->get_error_message(); return [ 'error' => true, 'content' => esc_html__("Something went wrong: ", 'awesome-users') . $errorMessage, ]; } // $responseCode = \wp_remote_retrieve_response_code($response); // $response_headers = wp_remote_retrieve_headers($response); $responseBody = \wp_remote_retrieve_body($response); return json_decode($responseBody); } }
单例Trait
<?php // -*- coding: utf-8 -*- declare(strict_types=1); namespace Sayem\Users\Traits; /** * singleton purpose trait * * * @author sayedulsayem * @since 1.0.0 */ trait Singleton { private static $instance; public static function instance(): object { if (!self::$instance) { self::$instance = new self(); } return self::$instance; } }
测试类(报错版本)
<?php declare(strict_types=1); namespace Sayem\Users\Tests\Unit; use Brain\Monkey; use Sayem\Users\CacheManager; class CacheManagerTest extends AbstractUnitTestcase { public function testUsersCacheIfCacheDoesNotExist() { $freshData = [ (object)['id' => 4, 'name' => 'Dave'], (object)['id' => 5, 'name' => 'Eve'], ]; Monkey\Functions\when('RestApiHandler::instance()->allUsersApiRequest')->justReturn($freshData); Monkey\Functions\when('get_transient')->justReturn(false); Monkey\Functions\when('set_transient')->justReturn(true); $cacheManager = CacheManager::instance(); $result = $cacheManager->usersCache(); $this->assertEquals($freshData, $result); } }
报错信息
There was 1 error:
- Sayem\Users\Tests\Unit\CacheManagerTest::testUsersCacheIfCacheDoesNotExist
Brain\Monkey\Name\Exception\InvalidName: ''RestApiHandler::instance()->allUsersApiRequest'' is not a valid function name.
解决方案
错误原因
Brain\Monkey的when()方法仅支持模拟全局函数,无法直接模拟类的方法调用链(如RestApiHandler::instance()->allUsersApiRequest()),这是导致报错的核心原因。
方法一:模拟单例类的实例
利用PHPUnit的Mock功能创建RestApiHandler的模拟实例,然后替换单例Trait中的静态$instance属性,让RestApiHandler::instance()返回这个模拟实例:
<?php declare(strict_types=1); namespace Sayem\Users\Tests\Unit; use Brain\Monkey; use PHPUnit\Framework\MockObject\MockObject; use Sayem\Users\CacheManager; use Sayem\Users\RestApiHandler; class CacheManagerTest extends AbstractUnitTestcase { public function testUsersCacheIfCacheDoesNotExist() { $freshData = [ (object)['id' => 4, 'name' => 'Dave'], (object)['id' => 5, 'name' => 'Eve'], ]; // 创建RestApiHandler的Mock实例 /** @var MockObject&RestApiHandler $apiHandlerMock */ $apiHandlerMock = $this->createMock(RestApiHandler::class); $apiHandlerMock->method('allUsersApiRequest')->willReturn($freshData); // 替换单例的静态实例 $reflection = new \ReflectionClass(RestApiHandler::class); $instanceProperty = $reflection->getProperty('instance'); $instanceProperty->setAccessible(true); $instanceProperty->setValue(null, $apiHandlerMock); // 模拟WP全局函数 Monkey\Functions\when('get_transient')->justReturn(false); Monkey\Functions\when('set_transient')->justReturn(true); $cacheManager = CacheManager::instance(); $result = $cacheManager->usersCache(); $this->assertEquals($freshData, $result); // 测试后重置单例实例,避免影响其他测试 $instanceProperty->setValue(null, null); } }
方法二:重构代码,使用依赖注入
单例模式会增加测试难度,更优的方案是重构CacheManager,通过构造函数注入RestApiHandler实例,而非直接依赖单例:
重构后的CacheManager
<?php // -*- coding: utf-8 -*- declare(strict_types=1); namespace Sayem\Users; class CacheManager { use Traits\Singleton; private $usersCacheKey = 'au_users_data'; private $userCacheKey = 'au_user_data'; private $settings; private $apiHandler; // 通过构造函数注入RestApiHandler实例 public function __construct(RestApiHandler $apiHandler = null) { $this->settings = ['cache-lifespan' => 6]; // 兼容原有逻辑,未注入时使用单例 $this->apiHandler = $apiHandler ?? RestApiHandler::instance(); } public function usersCache(): array { $refreshData = false; $cachedData = get_transient($this->usersCacheKey); if (false === $cachedData) { $refreshData = true; } else { $data = unserialize($cachedData); if (isset($data['error']) && $data['error']) { $refreshData = true; } } if ($refreshData) { // 使用注入的实例调用方法 $allUsers = $this->apiHandler->allUsersApiRequest(); set_transient( $this->usersCacheKey, serialize($allUsers), $this->hoursInSecond((int)$this->settings['cache-lifespan']) ); return $allUsers; } return unserialize($cachedData); } }
重构后的测试类
<?php declare(strict_types=1); namespace Sayem\Users\Tests\Unit; use Brain\Monkey; use PHPUnit\Framework\MockObject\MockObject; use Sayem\Users\CacheManager; use Sayem\Users\RestApiHandler; class CacheManagerTest extends AbstractUnitTestcase { public function testUsersCacheIfCacheDoesNotExist() { $freshData = [ (object)['id' => 4, 'name' => 'Dave'], (object)['id' => 5, 'name' => 'Eve'], ]; // 创建RestApiHandler的Mock实例 /** @var MockObject&RestApiHandler $apiHandlerMock */ $apiHandlerMock = $this->createMock(RestApiHandler::class); $apiHandlerMock->method('allUsersApiRequest')->willReturn($freshData); // 直接注入Mock实例到CacheManager $cacheManager = new CacheManager($apiHandlerMock); // 模拟WP全局函数 Monkey\Functions\when('get_transient')->justReturn(false); Monkey\Functions\when('set_transient')->justReturn(true); $result = $cacheManager->usersCache(); $this->assertEquals($freshData, $result); } }
内容的提问来源于stack exchange,提问作者Sayedul Sayem
相关产品推荐
相关产品推荐

