如何在 PHPUnit 覆盖测试中模拟变量?

How to mock variables in a PHPUnit Coverage Test?

我正在编写 PHPUnit 测试和 运行 覆盖率测试。我承认很难达到 100% 的覆盖率,但是,我希望尽可能接近。在以下场景中,如何在子句中模拟变量以测试代码块?

class CalendarClientService
{
    /** @var array SCOPES */
    public const SCOPES = [Google_Service_Calendar::CALENDAR];

    /** @var string ACCESS_TYPE */
    public const ACCESS_TYPE = "offline";

    /** @var string CALENDAR_ID */
    public const CALENDAR_ID = "primary";

    /** @var int MAX_RESULTS */
    public const MAX_RESULTS = 25;

    /** @var string ORDER_BY */
    public const ORDER_BY = "startTime";

    /** @var bool SINGLE_EVENTS */
    public const SINGLE_EVENTS = true;

    /** @var string|null TIME_MIN */
    public const TIME_MIN = null;

    /** @var bool CACHE_TIME_TO_LIVE */
    public const CACHE_TIME_TO_LIVE = 604800;

    /** @var string */
    public string $clientSecretPath = "";

    /** @var StorageAdapterFactoryInterface */
    protected StorageAdapterFactoryInterface $storageAdapterFactory;

    /** @var StorageInterface */
    protected StorageInterface $storageInterfaceCache;

    /**
     * CalendarClientService constructor.
     * @param string $clientSecretPath
     * @param StorageAdapterFactoryInterface $storageAdapterFactory
     * @param StorageInterface $storageInterfaceCache
     */
    public function __construct(
        string $clientSecretPath,
        StorageAdapterFactoryInterface $storageAdapterFactory,
        StorageInterface $storageInterfaceCache
    ) {
        $this->clientSecretPath = $clientSecretPath;
        $this->storageAdapterFactory = $storageAdapterFactory;
        $this->storageInterfaceCache = $storageInterfaceCache;
    }

    /** @return string */
    public function getClientSecretPath()
    {
        return $this->clientSecretPath;
    }

    /** @param string $secretFile */
    public function setClientSecretPath(string $secretFile)
    {
        $this->clientSecretPath = $secretFile;
    }

    /**
     * @param array
     * @return Google_Service_Calendar_Event
     */
    public function getGoogleServiceCalendarEvent($eventData)
    {
        return new Google_Service_Calendar_Event($eventData);
    }

    /**
     * @param string
     * @return Google_Service_Calendar_EventDateTime
     */
    public function getGoogleServiceCalendarEventDateTime($dateTime)
    {
        $eventDateTime = new Google_Service_Calendar_EventDateTime();
        $eventDateTime->setDateTime(Carbon::parse($dateTime)->toW3cString());
        $eventDateTime->setTimeZone(Carbon::parse($dateTime)->timezone->getName());
        return $eventDateTime;
    }

    /**
     * @param Google_Client $client
     * @return Events
     */
    public function getGoogleServiceCalendarResourceEvents(Google_Client $client)
    {
        $service = new Google_Service_Calendar($client);
        return $service->events;
    }

    /**
     * @param int
     * @return array
     * @throws Exception
     * @throws ExceptionInterface
     */
    public function getEventData($id)
    {
        $client = $this->getClient();
        if (!$this->authenticateClient($client)) {
            return [
                "error" => "authentication",
                "url" => filter_var($client->createAuthUrl(), FILTER_SANITIZE_URL),
            ];
        }
        $service = $this->getGoogleServiceCalendarResourceEvents($client);
        return ["event" => $service->get(self::CALENDAR_ID, $id)];
    }

    /**
     * @return Google_Client
     * @throws Exception
     */
    public function getClient()
    {
        $client = new Google_Client();
        $client->setApplicationName(Module::MODULE_NAME);
        $client->setScopes(self::SCOPES);
        $client->setAuthConfig($this->clientSecretPath);
        $client->setAccessType(self::ACCESS_TYPE);
        return $client;
    }

    /**
     * @param Google_Client $client
     * @return bool
     * @throws ExceptionInterface
     */
    public function authenticateClient(Google_Client $client)
    {
        if ($this->storageInterfaceCache->hasItem("api_access_token")) {
            $accessToken = json_decode($this->storageInterfaceCache->getItem("api_access_token"), true);
            if ($accessToken["error"] == "invalid_grant" || empty($accessToken)) {
                $this->storageInterfaceCache->removeItem("api_access_token");
            } else {
                $this->storageInterfaceCache->setItem("api_access_token", json_encode($accessToken));
                $client->setAccessToken($accessToken);
            }
        }
        if ($client->isAccessTokenExpired()) {
            $tokenValid = false;
            if ($client->getRefreshToken()) {
                $client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
                $accessToken = $client->getAccessToken();
                $this->storageInterfaceCache->setItem("api_access_token", json_encode($accessToken));
                $tokenValid = true;
            } else {
                $helper = new Helper();
                if(!$helper->verifyAuthCode($_GET["code"])){
                    return $tokenValid;
                }
                $authCode = $_GET["code"];
                $accessToken = $client->fetchAccessTokenWithAuthCode($authCode);
                if ($accessToken["error"] == "invalid_grant" || empty($accessToken)) {
                    $this->storageInterfaceCache->removeItem("api_access_token");
                } else {
                    $this->storageInterfaceCache->setItem("api_access_token", json_encode($accessToken));
                    $client->setAccessToken($accessToken);
                    $tokenValid = true;
                }
            }
        } else {
            $tokenValid = true;
        }

        return isset($tokenValid) ? $tokenValid : false;
    }

我想在 authenticateClient 方法中测试从顶部开始的第 6 行,并想模拟此子句 $accessToken["error"] == "invalid_grant" || empty($accessToken)

现在怎么办?

编辑: 这是我编写的测试。现在无论我在 $this->storageInterfaceCacheMock->method("getItem") 中模拟什么值,它总是 returns 空 $accessToken。我还附上了图片,以便更好地了解正在发生的事情和我想要什么。

    public function testGetEventDataReturnsArrayOnSuccessfulAuthenticateClientThroughCache()
    {
        $this->storageInterfaceCacheMock->method("hasItem")->willReturn(true);
        $this->storageInterfaceCacheMock->method("getItem")->willReturn(json_encode('{"access_token":"ya29.a0ARrdaM99pJTf1XzmD1ngxAH3XJud8lvHb0aTaOOABYdfdhsdfgsdfgVD9OoH4heiKoskDF7DMkHj1_aPuWIO5TE14KHJidFf66xwn_pTCkkSow6Kg4lRHwGrNQBQGI8sPlgnFO5U5hJvYdqgxDMHEqw1TER2w","expires_in":3599,"scope":"https:\/\/www.googleapis.com\/auth\/calendar","token_type":"Bearer","created":1637312218,"refresh_token":"1\/\/03psr5omKiljUCgYFDHDGJHGSHSNwF-L9Iraor5zcfe-h3BeCHSFGSDFGDGJHjy4UnEtKj974LXthS5bWexQcjviVGfJsdfGHSHgIrDn6Yk"}'));

        $this->assertIsArray($this->calendarClientService->getEventData(1));
    }

下面提到了另一个未按要求执行的测试。 (在屏幕截图中也可见)

    public function testAccessTokenIsExpiredAndGotRefreshToken()
    {
        $this->googleClientMock->method("isAccessTokenExpired")->willReturn(true);
        $this->googleClientMock->method("getRefreshToken")->willReturn(true);
        $this->googleClientMock->method("fetchAccessTokenWithRefreshToken")->willReturnSelf();
        $this->googleClientMock->method("getAccessToken")->willReturnSelf();
        $this->assertTrue($this->calendarClientService->authenticateClient($this->googleClientMock));
    }

我相信你想要json_decode你在这里不需要双重编码:

     $this->storageInterfaceCacheMock->method("getItem")->willReturn(json_encode('{"access_token":"...."}'));

刚刚

$this->storageInterfaceCacheMock->method("getItem")->willReturn('{"access_token":"...."}');

这是我解决问题并使测试成功的方法。

我声明了一个虚拟 ACCESS_TOKEN 如下,然后在测试方法中使用。

class CalendarControllerTest extends AbstractApplicationTestCase
{
    /** @var string CLIENT_SECRET */
    public const CLIENT_SECRET = __DIR__ . "/../_fixtures/config/client_secret.json";

    /** @var string CLIENT_SECRET */
    public const ACCESS_TOKEN = [
        "access_token" => "test-data",
        "expires_in" => 3592,
        "scope" => "https://www.googleapis.com/auth/calendar",
        "token_type" => "Bearer",
        "created" => 1640858809,
    ];
    ...
    ...
   ...
}
    public function setUp(): void
    {
        parent::setUp();
        $this->googleClientMock = $this->getMockBuilder(Google_Client::class)
            ->disableOriginalConstructor()
            ->onlyMethods(
                [
                    "isAccessTokenExpired",
                    "setAuthConfig",
                    "getRefreshToken",
                    "fetchAccessTokenWithRefreshToken",
                    "fetchAccessTokenWithAuthCode",
                    "getAccessToken",
                    "setAccessToken",
                ]
            )
            ->getMock();
        $this->googleServiceCalendarResourceEventsMock = $this->getMockBuilder(Events::class)
            ->disableOriginalConstructor()
            ->onlyMethods(["get"])
            ->getMock();
        $this->googleServiceCalendarEventMock = $this->getMockBuilder(Event::class)
            ->disableOriginalConstructor()
            ->getMock();
        $this->storageInterfaceCacheMock = $this->getMockForAbstractClass(StorageInterface::class);

        $this->container->setAllowOverride(true);
        $this->container->setService(Google_Client::class, $this->googleClientMock);
        $this->container->setService(Events::class, $this->googleServiceCalendarResourceEventsMock);
        $this->container->setService(Event::class, $this->googleServiceCalendarEventMock);
        $this->container->setService(StorageInterface::class, $this->storageInterfaceCacheMock);
        $this->container->setAllowOverride(true);

        $this->googleClientMock->method("setAuthConfig")->willReturn(true);

        $this->calendarClientService = $this->container->get("ServiceManager")->get(CalendarClientService::class);
        $this->calendarClientService->setClientSecretPath(CalendarControllerTest::CLIENT_SECRET);
    }
    
    /** @throws ExceptionInterface */
    public function testAccessTokenIsExpiredAndFailureToRefreshTokenWillGenerateNewAccessToken()
    {
        $this->calendarClientService->setAuthCode(CalendarControllerTest::DEFAULT_TESTING_VALUE);
        $this->googleClientMock->method("isAccessTokenExpired")->willReturn(true);
        $this->googleClientMock->method("getRefreshToken")->willReturn(false);
        $this->googleClientMock->method("fetchAccessTokenWithAuthCode")->willReturn(
            CalendarControllerTest::ACCESS_TOKEN
        );
        $this->storageInterfaceCacheMock->method("setItem")->willReturn(true);
        $this->googleClientMock->method("setAccessToken")->willReturnSelf();

        $this->assertTrue($this->calendarClientService->authenticateClient($this->googleClientMock));
    }