You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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:

  1. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 21:15:00