用PHPUnit模拟guzzle和测试外部API的详细指南

280 阅读2分钟

在这个例子中,我们将测试一个外部API的模拟和不模拟Guzzle客户端。我们现在都知道,在测试环境中,外部API调用必须被模拟。然而,出于演示的目的,我将向你展示如果我们不模拟它,我们的测试会是什么样子。

真实的API响应

{
  "status": 200,
  "result": {
    "postcode": "REAL POSTCODE",
    "quality": 1,
    "eastings": 1234,
    "northings": 12345,
    "country": "England",
    "nhs_ha": "London",
    "longitude": -0.123456789,
    "latitude": 1.12345678,
    "european_electoral_region": "London",
    "primary_care_trust": "City",
    "region": "London",
    "lsoa": "City",
    "msoa": "City 123",
    "incode": "POSTCODE",
    "outcode": "REAL",
    "parliamentary_constituency": "City Central",
    "admin_district": "City",
    "parish": "City, unparished area",
    "admin_county": null,
    "admin_ward": "Westminister",
    "ccg": "NHS City",
    "nuts": "City",
    "codes": {
      "admin_district": "U1234567",
      "admin_county": "U12345678",
      "admin_ward": "U1234567",
      "parish": "U123456",
      "parliamentary_constituency": "U12345678",
      "ccg": "U1234567",
      "nuts": "UK1234"
    }
  }
}

异常

namespace Application\Exception;

use Exception;

class PostcodesException extends Exception
{
    public function __construct($message, $code = 400)
    {
        parent::__construct($message, $code);
    }
}

邮政编码

这个类调用外部API来获取英国的邮编信息,我们将测试它。

namespace Application\Util;

use Application\Exception\PostcodesException;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Stream;

class Postcodes
{
    private $client;
    private $baseUri;

    public function __construct(Client $client, $baseUri)
    {
        $this->client = $client;
        $this->baseUri = $baseUri;
    }

    public function getData($postcode)
    {
        if (!$postcode) {
            $this->throwException('Postcode is required.');
        }

        try {
            /** @var Response $response */
            $response = $this->client->request('GET', $this->baseUri.$postcode);
            /** @var Stream $body */
            $body = $response->getBody();

            return $body->getContents();
        } catch (ClientException $e) {
            $this->throwException(sprintf('Failed to get postcode data for "%s".', $postcode));
        }
    }

    private function throwException($message, $code = 400)
    {
        throw new PostcodesException($message, $code);
    }
}

没有模拟

namespace tests\Application\Util;

use Application\Exception\PostcodesException;
use Application\Util\Postcodes;
use GuzzleHttp\Client;
use PHPUnit\Framework\TestCase;

class PostcodesTest extends TestCase
{
    /** @var Postcodes */
    private $postcodes;

    protected function setUp()
    {
        $client = new Client();
        $baseUri = 'http://api.postcodes.io/postcodes/';

        $this->postcodes = new Postcodes($client, $baseUri);
    }

    protected function tearDown()
    {
        $this->postcodes = null;
    }

    public function testShouldThrowExceptionForEmptyPostcodeArgument()
    {
        $this->expectException(PostcodesException::class);
        $this->expectExceptionMessage('Postcode is required.');
        $this->expectExceptionCode(400);

        $this->postcodes->getData('');
    }

    public function testShouldThrowExceptionForInvalidPostcodeArgument()
    {
        $postcode = 'INVALID';

        $this->expectException(PostcodesException::class);
        $this->expectExceptionMessage(sprintf('Failed to get postcode data for "%s".', $postcode));
        $this->expectExceptionCode(400);

        $this->postcodes->getData($postcode);
    }

    public function testShouldReturnPostcodeData()
    {
        $expected = file_get_contents(__DIR__.'/Mock/Postcodes/response-body.txt');

        $result = $this->postcodes->getData('REAL POSTCODE');

        $this->assertEquals($expected, $result);
    }
}
#tests/Application/Util/Mock/Postcodes/response-body.txt

Content of this file is exactly the same as Real API response I added above.

测试

$ vendor/bin/phpunit --filter PostcodesTest tests/Application/Util/PostcodesTest.php
PHPUnit 5.7.22 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)

Time: 97 ms, Memory: 4.25MB

OK (3 tests, 5 assertions)

有嘲讽

更多信息,请访问测试Guzzle客户端

namespace tests\Application\Util;

use Application\Exception\PostcodesException;
use Application\Util\Postcodes;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;

class PostcodesTest extends TestCase
{
    public function testShouldThrowExceptionForEmptyPostcodeArgument()
    {
        $this->expectException(PostcodesException::class);
        $this->expectExceptionMessage('Postcode is required.');
        $this->expectExceptionCode(400);

        $postcodes = $this->getPostcodes(400);

        $postcodes->getData('');
    }

    public function testShouldThrowExceptionForInvalidPostcodeArgument()
    {
        $postcode = 'INVALID';

        $this->expectException(PostcodesException::class);
        $this->expectExceptionMessage(sprintf('Failed to get postcode data for "%s".', $postcode));
        $this->expectExceptionCode(400);

        $postcodes = $this->getPostcodes(400);

        $postcodes->getData($postcode);
    }

    public function testShouldReturnPostcodeData()
    {
        $body = file_get_contents(__DIR__.'/Mock/Postcodes/response-body.txt');

        $postcodes = $this->getPostcodes(200, $body);

        $result = $postcodes->getData('XYZ XYZ');

        $this->assertEquals($body, $result);
    }

    private function getPostcodes($status, $body = null)
    {
        $mock = new MockHandler([new Response($status, [], $body)]);
        $handler = HandlerStack::create($mock);
        $client = new Client(['handler' => $handler]);

        return new Postcodes($client, 'http://mocked.postcodes.xyz/');
    }
}
#tests/Application/Util/Mock/Postcodes/response-body.txt

{
  "status": 200,
  "result": {
    "postcode": "XYZ XYZ",
    "longitude": -0.000000,
    "latitude": 1.111111
  }
}

测试

$ vendor/bin/phpunit --filter PostcodesTest tests/Application/Util/PostcodesTest.php
PHPUnit 5.7.22 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)

Time: 97 ms, Memory: 4.25MB

OK (3 tests, 5 assertions)