如何在 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));
}
我正在编写 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));
}