Проверяем функцию и утверждаем, что она вызывает другую внутри того же класса - PullRequest
0 голосов
/ 29 октября 2018

Если предположить, что класс ApiClient тестируется, как я могу утверждать, что storeToken() правильно вызывается в retrieveNewToken(), моя проблема в том, что я не могу использовать метод expects(), так как $this->testedInstance не является ложным, который устанавливается с помощью:

ApiClient

private function retrieveNewToken()
{
        $response = $this->client->post(
            'login',
            [
                'json' => [
                    'username' => $this->apiLogin,
                    'password' => $this->apiPassword,
                ],
            ]
        );
        $statusCode = $response->getStatusCode();
        $responseBody = json_decode($response->getBody());

        if (Response::HTTP_OK === $statusCode) {
            $this->storeToken($responseBody->token);

            return $responseBody->token;
        }

        $exception = sprintf(
            'Error retrieving token : %s',
            $responseBody->message
        );
        $this->logger->error($exception);
        throw new \Exception($exception);
    }

private function storeToken($token)
{
    $tokenCacheItem = $this->memcache->getItem('token');
    $tokenCacheItem->set($token);
    if (!$this->memcache->save($tokenCacheItem)) {
        throw new \Exception('Error saving token');
    }
}

Прямо сейчас мой тест:

ApiClientTest

public function setUp()
{
    $this->testedInstance = new ApiClient($dependencies);
}

public function tearDown()
{
    $this->testedInstance = null;
}

public function retrieveNewTokenOK(){
    $entityMock = $this->getMockBuilder(ApiClient::class)
        ->setConstructorArgs([
            $dependencies;
        ])
        ->setMethods(['storeToken', 'prepareClient'])
        ->getMock();

    $guzzleClientMock = $this->getMockBuilder(Client::class)
        ->setConstructorArgs([
            [
            'base_uri' => $this->baseUri,
            'proxy' => ''
            ]
        ])->getMock();

    $entityMock->expects($this->once())->method('prepareClient')->willReturn($guzzleClientMock);

    $responseMock = $this->getMockBuilder(Response::class)->setConstructorArgs(['', Response::HTTP_OK])->getMock();
    $responseMock->expects($this->once())->method('getStatusCode')->willReturn(Response::HTTP_OK);
    $responseStreamMock = $this->createMock(StreamInterface::class);
    $jsonDecode = $this->getFunctionMock(ApiClient::class, 'json_decode');
    $jsonDecode->expects($this->any())->with($responseStreamMock)->willReturn((object) ['token' => 'JWTTOKEN']);
    $entityMock->expects($this->once())->method('storeToken')->with('JWTTOKEN');

    $this->invokeMethod($entityMock, 'retrieveNewToken');
}

1 Ответ

0 голосов
/ 31 октября 2018

Я понимаю вашу борьбу. Я был там много раз и думаю, что смогу вам помочь. Обычно, когда какой-то фрагмент кода сложно протестировать, он просто нуждается в некоторой реструктуризации на более мелкие части. Я позволил себе немного перестроить код, чтобы сделать его более тестируемым. Здесь вы найдете результат, я объясню его более подробно.

Код производства:

<?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 будет ясно, что он вернет новый токен.

Надеюсь, это имеет смысл.

ура!

...