Я понимаю вашу борьбу. Я был там много раз и думаю, что смогу вам помочь. Обычно, когда какой-то фрагмент кода сложно протестировать, он просто нуждается в некоторой реструктуризации на более мелкие части. Я позволил себе немного перестроить код, чтобы сделать его более тестируемым. Здесь вы найдете результат, я объясню его более подробно.
Код производства:
<?php
declare(strict_types=1);
namespace App;
use GuzzleHttp\Client;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response;
class ApiClient
{
/**
* @var string
*/
private $apiLogin;
/**
* @var string
*/
private $apiPassword;
/**
* @var Client
*/
private $client;
/**
* @var LoggerInterface
*/
private $logger;
/**
* @var TokenStorage
*/
private $tokenStorage;
public function __construct(
string $apiLogin,
string $apiPassword,
Client $client,
LoggerInterface $logger,
TokenStorage $tokenStorage
) {
$this->apiLogin = $apiLogin;
$this->apiPassword = $apiPassword;
$this->client = $client;
$this->logger = $logger;
$this->tokenStorage = $tokenStorage;
}
public function retrieveNewToken(): string
{
$response = $this->client->post('login', [
'json' => [
'username' => $this->apiLogin,
'password' => $this->apiPassword,
]
]);
$body = json_decode($response->getBody()->getContents());
if (Response::HTTP_OK !== $response->getStatusCode()) {
$errorMessage = sprintf('Error retrieving token: %s', $body->message);
$this->logger->error($errorMessage);
throw new \Exception($errorMessage);
}
$this->tokenStorage->storeToken($body->token);
return $body->token;
}
}
И тестовый класс:
<?php
declare(strict_types=1);
namespace App;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
class ApiClientTest extends TestCase
{
/**
* @var ApiClient
*/
private $apiClient;
/**
* @var Client|\PHPUnit_Framework_MockObject_MockObject
*/
private $client;
/**
* @var LoggerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
private $logger;
/**
* @var TokenStorage|\PHPUnit_Framework_MockObject_MockObject
*/
private $tokenStorage;
protected function setUp(): void
{
$this->client = $this->createMock(Client::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->tokenStorage = $this->createMock(TokenStorage::class);
$this->apiClient = new ApiClient('test', 'test', $this->client, $this->logger, $this->tokenStorage);
}
public function testCanRetrieveNewToken(): void
{
$response = new Response(200, [], '{"token":"test-token"}');
$this->client->expects($this->once())
->method('post')
->willReturn($response);
$this->tokenStorage->expects($this->once())
->method('storeToken')
->with('test-token');
$token = $this->apiClient->retrieveNewToken();
self::assertEquals('test-token', $token);
}
/**
* @expectedException \Exception
* @expectedExceptionMessage Error retrieving token: Something went wrong
*/
public function testThrowsExceptionOnUnexpectedStatusCode(): void
{
$response = new Response(400, [], '{"message":"Something went wrong"}');
$this->client->expects($this->once())
->method('post')
->willReturn($response);
$this->apiClient->retrieveNewToken();
}
}
Как видите, я извлек логику хранения токенов в собственный класс. Это должно следовать принципу единой ответственности, который заключается в создании небольших классов, которые делают только одно. Это решение также делает ApiClient более тестируемым, поскольку теперь вы можете просто утверждать, что при получении нового токена класс TokenStorage вызывается с правильным токеном. Реализация класса TokenStorage также будет иметь свой собственный тест, в котором вы можете смоделировать класс Memcache.
Еще один момент, на который следует обратить внимание, - это то, что я изменил путь по умолчанию для функции с выброса исключения на возврат нового токена. Это связано с чистым кодом и тем, как люди привыкли читать код. При первом чтении метода retrieveToken
будет ясно, что он вернет новый токен.
Надеюсь, это имеет смысл.
ура!