Skip to content

Commit

Permalink
feature #34871 [HttpClient] Allow pass array of callable to the mocki…
Browse files Browse the repository at this point in the history
…ng 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
-------

a36797d Allow pass array of callable to the mocking http client
  • Loading branch information
nicolas-grekas committed Feb 2, 2020
2 parents 985f64b + a36797d commit 00b6846
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 2 deletions.
12 changes: 10 additions & 2 deletions src/Symfony/Component/HttpClient/MockHttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -64,9 +65,11 @@ public function request(string $method, string $url, array $options = []): Respo
} 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);
}
Expand All @@ -84,4 +87,9 @@ public function stream($responses, float $timeout = null): ResponseStreamInterfa

return new ResponseStream(MockResponse::stream($responses, $timeout));
}

public function getRequestsCount(): int
{
return $this->requestsCount;
}
}
121 changes: 121 additions & 0 deletions src/Symfony/Component/HttpClient/Tests/MockHttpClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,127 @@

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 = [];
Expand Down

0 comments on commit 00b6846

Please sign in to comment.