feature #34871 [HttpClient] Allow pass array of callable to the mocking http client (Koc)

This PR was merged into the 5.1-dev branch.

Discussion
----------

[HttpClient] Allow pass array of callable to the mocking http client

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | not yet

For the now MockHttpClient allows pass closure as response factory. It useful for tests to perform assertions that expected request was sent. But If we are sending multiple sequental requests then it became a little bit tricky to perform assertions:

```php
<?php

$requestIndex = 0;
$expectedRequest = function ($method, $url, $options) use (&$requestIndex) {
    switch (++$requestIndex) {
        case 1:
            $this->assertSame('GET', $method);
            $this->assertSame('https://example.com/api/v1/customer', $url);

            return new MockResponse(CustomerFixture::CUSTOMER_RESPONSE);

        case 2:
            $this->assertSame('POST', $method);
            $this->assertSame('https://example.com/api/v1/customer/1/products', $url);
            $this->assertJsonStringEqualsJsonFile(CustomerFixture::CUSTOMER_PRODUCT_PAYLOAD, $options['json']);

            return new MockResponse();

        default:
            throw new \InvalidArgumentException('Too much requests');
    }
};

$client = new MockHttpClient($expectedRequest);
static::$container->set('http_client.example', $client);

$commandTester->execute(['--since' => '2019-01-01 00:05:00', '--until' => '2019-01-01 00:35:00']);

$this->assertSame(2, $requestIndex, 'All expected requests was sent.');
```

This PR introduces possibility to define multiple callable response factories and `getSentRequestsCount` method to make sure that each factory was called:

```php
<?php

$expectedRequests = [
    function ($method, $url, $options) {
        $this->assertSame('GET', $method);
        $this->assertSame('https://example.com/api/v1/customer', $url);

        return new MockResponse(CustomerFixture::CUSTOMER_RESPONSE);
    },
    function ($method, $url, $options) {
        $this->assertSame('POST', $method);
        $this->assertSame('https://example.com/api/v1/customer/1/products', $url);
        $this->assertJsonStringEqualsJsonFile(CustomerFixture::CUSTOMER_PRODUCT_PAYLOAD, $options['json']);

        return new MockResponse();
    },
];

$client = new MockHttpClient($expectedRequest);
static::$container->set('http_client.example', $client);

$commandTester->execute(['--since' => '2019-01-01 00:05:00', '--until' => '2019-01-01 00:35:00']);

$this->assertSame(2, $client->getSentRequestsCount(), 'All expected requests was sent.');
```

Also it adds a lot of tests.

Commits
-------

a36797d60e Allow pass array of callable to the mocking http client
This commit is contained in:
Nicolas Grekas 2020-02-02 12:07:05 +01:00
commit 00b6846978
2 changed files with 131 additions and 2 deletions

View File

@ -29,9 +29,10 @@ class MockHttpClient implements HttpClientInterface
private $responseFactory;
private $baseUri;
private $requestsCount = 0;
/**
* @param callable|ResponseInterface|ResponseInterface[]|iterable|null $responseFactory
* @param callable|callable[]|ResponseInterface|ResponseInterface[]|iterable|null $responseFactory
*/
public function __construct($responseFactory = null, string $baseUri = null)
{
@ -64,9 +65,11 @@ class MockHttpClient implements HttpClientInterface
} elseif (!$this->responseFactory->valid()) {
throw new TransportException('The response factory iterator passed to MockHttpClient is empty.');
} else {
$response = $this->responseFactory->current();
$responseFactory = $this->responseFactory->current();
$response = \is_callable($responseFactory) ? $responseFactory($method, $url, $options) : $responseFactory;
$this->responseFactory->next();
}
++$this->requestsCount;
return MockResponse::fromRequest($method, $url, $options, $response);
}
@ -84,4 +87,9 @@ class MockHttpClient implements HttpClientInterface
return new ResponseStream(MockResponse::stream($responses, $timeout));
}
public function getRequestsCount(): int
{
return $this->requestsCount;
}
}

View File

@ -22,6 +22,127 @@ use Symfony\Contracts\HttpClient\ResponseInterface;
class MockHttpClientTest extends HttpClientTestCase
{
/**
* @dataProvider mockingProvider
*/
public function testMocking($factory, array $expectedResponses)
{
$client = new MockHttpClient($factory, 'https://example.com/');
$this->assertSame(0, $client->getRequestsCount());
$urls = ['/foo', '/bar'];
foreach ($urls as $i => $url) {
$response = $client->request('POST', $url, ['body' => 'payload']);
$this->assertEquals($expectedResponses[$i], $response->getContent());
}
$this->assertSame(2, $client->getRequestsCount());
}
public function mockingProvider(): iterable
{
yield 'callable' => [
static function (string $method, string $url, array $options = []) {
return new MockResponse($method.': '.$url.' (body='.$options['body'].')');
},
[
'POST: https://example.com/foo (body=payload)',
'POST: https://example.com/bar (body=payload)',
],
];
yield 'array of callable' => [
[
static function (string $method, string $url, array $options = []) {
return new MockResponse($method.': '.$url.' (body='.$options['body'].') [1]');
},
static function (string $method, string $url, array $options = []) {
return new MockResponse($method.': '.$url.' (body='.$options['body'].') [2]');
},
],
[
'POST: https://example.com/foo (body=payload) [1]',
'POST: https://example.com/bar (body=payload) [2]',
],
];
yield 'array of response objects' => [
[
new MockResponse('static response [1]'),
new MockResponse('static response [2]'),
],
[
'static response [1]',
'static response [2]',
],
];
yield 'iterator' => [
new \ArrayIterator(
[
new MockResponse('static response [1]'),
new MockResponse('static response [2]'),
]
),
[
'static response [1]',
'static response [2]',
],
];
yield 'null' => [
null,
[
'',
'',
],
];
}
/**
* @dataProvider transportExceptionProvider
*/
public function testTransportExceptionThrowsIfPerformedMoreRequestsThanConfigured($factory)
{
$client = new MockHttpClient($factory, 'https://example.com/');
$client->request('POST', '/foo');
$client->request('POST', '/foo');
$this->expectException(TransportException::class);
$client->request('POST', '/foo');
}
public function transportExceptionProvider(): iterable
{
yield 'array of callable' => [
[
static function (string $method, string $url, array $options = []) {
return new MockResponse();
},
static function (string $method, string $url, array $options = []) {
return new MockResponse();
},
],
];
yield 'array of response objects' => [
[
new MockResponse(),
new MockResponse(),
],
];
yield 'iterator' => [
new \ArrayIterator(
[
new MockResponse(),
new MockResponse(),
]
),
];
}
protected function getHttpClient(string $testCase): HttpClientInterface
{
$responses = [];