测试一个函数并断言它在同一类中调用了另一个

时间:2018-10-29 09:46:51

标签: unit-testing mocking phpunit

假设正在测试一个类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 :(得分:0)

我了解您的挣扎。我去过很多次,我想可以为您提供帮助。通常,当某些代码难以测试时,只需要将其重组为较小的部分即可。我自由地重组了一些代码,使其更具可测试性。在这里您可以找到结果,我将对其进行详细说明。

生产代码:

<?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类。

要注意的另一点是,我将函数的默认路径从引发Exception更改为返回新令牌。这与清理代码以及人们习惯于阅读代码有关。首次读取retrieveToken方法时,很明显它将返回新令牌。

我希望这是有道理的。

干杯!