Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d5d1b50

Browse files
committed
feature #30604 [HttpClient] add MockHttpClient (nicolas-grekas)
This PR was merged into the 4.3-dev branch. Discussion ---------- [HttpClient] add MockHttpClient | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - This PR introduces `MockHttpClient` and `MockResponse`, to be used for testing classes that need an HTTP client without making actual HTTP requests. `MockHttpClient` is configured via its constructor: you provide it either with an iterable or a callable, and these will be used to provide responses as the consumer requests them. Example: ```php $responses = [ new MockResponse($body1, $info1), new MockResponse($body2, $info2), ]; $client = new MockHttpClient($responses); $response1 = $client->request(...); // created from $responses[0] $response2 = $client->request(...); // created from $responses[1] ``` Or alternatively: ```php $callback = function ($method, $url, $options) { return new MockResponse(...); }; $client = new MockHttpClient($callback); $response = $client->request(...); // calls $callback internal ``` The responses provided to the client don't have to be instances of `MockResponse` - any `ResponseInterface` works (e.g. `$this->getMockBuilder(ResponseInterface::class)->getMock()`). Using `MockResponse` allows simulating chunked responses and timeouts: ```php $body = function () { yield 'hello'; yield ''; // the empty string is turned into a timeout so that they are easy to test yield 'world'; }; $mockResponse = new Mockresponse($body); ``` Last but not least, the implementation simulates the full lifecycle of a properly behaving `HttpClientInterface` contracts implementation: error handling, progress function, etc. This is "proved" by `MockHttpClientTest`, who implements and passes the reference test suite in `HttpClientTestCase`. Commits ------- 8fd7584 [HttpClient] add MockHttpClient
2 parents 72fa2b3 + 8fd7584 commit d5d1b50

File tree

10 files changed

+549
-53
lines changed

10 files changed

+549
-53
lines changed

src/Symfony/Component/HttpClient/Chunk/ErrorChunk.php

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,14 @@
2121
*/
2222
class ErrorChunk implements ChunkInterface
2323
{
24-
protected $didThrow;
25-
24+
private $didThrow = false;
2625
private $offset;
2726
private $errorMessage;
2827
private $error;
2928

30-
/**
31-
* @param bool &$didThrow Allows monitoring when the $error has been thrown or not
32-
*/
33-
public function __construct(bool &$didThrow, int $offset, \Throwable $error = null)
29+
public function __construct(int $offset, \Throwable $error = null)
3430
{
3531
$didThrow = false;
36-
$this->didThrow = &$didThrow;
3732
$this->offset = $offset;
3833
$this->error = $error;
3934
$this->errorMessage = null !== $error ? $error->getMessage() : 'Reading from the response stream reached the inactivity timeout.';
@@ -96,6 +91,14 @@ public function getError(): ?string
9691
return $this->errorMessage;
9792
}
9893

94+
/**
95+
* @return bool Whether the wrapped error has been thrown or not
96+
*/
97+
public function didThrow(): bool
98+
{
99+
return $this->didThrow;
100+
}
101+
99102
public function __destruct()
100103
{
101104
if (!$this->didThrow) {

src/Symfony/Component/HttpClient/HttpClientTrait.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ private static function prepareRequest(?string $method, ?string $url, array $opt
117117

118118
// Finalize normalization of options
119119
$options['headers'] = $headers;
120-
$options['http_version'] = (string) ($options['http_version'] ?? '');
120+
$options['http_version'] = (string) ($options['http_version'] ?? '') ?: null;
121121
$options['timeout'] = (float) ($options['timeout'] ?? ini_get('default_socket_timeout'));
122122

123123
return [$url, $options];
@@ -128,6 +128,8 @@ private static function prepareRequest(?string $method, ?string $url, array $opt
128128
*/
129129
private static function mergeDefaultOptions(array $options, array $defaultOptions, bool $allowExtraOptions = false): array
130130
{
131+
unset($options['raw_headers'], $defaultOptions['raw_headers']);
132+
131133
$options['headers'] = self::normalizeHeaders($options['headers'] ?? []);
132134

133135
if ($defaultOptions['headers'] ?? false) {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpClient;
13+
14+
use Symfony\Component\HttpClient\Exception\TransportException;
15+
use Symfony\Component\HttpClient\Response\MockResponse;
16+
use Symfony\Component\HttpClient\Response\ResponseStream;
17+
use Symfony\Contracts\HttpClient\HttpClientInterface;
18+
use Symfony\Contracts\HttpClient\ResponseInterface;
19+
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
20+
21+
/**
22+
* A test-friendly HttpClient that doesn't make actual HTTP requests.
23+
*
24+
* @author Nicolas Grekas <[email protected]>
25+
*/
26+
class MockHttpClient implements HttpClientInterface
27+
{
28+
use HttpClientTrait;
29+
30+
private $responseFactory;
31+
private $baseUri;
32+
33+
/**
34+
* @param callable|ResponseInterface|ResponseInterface[]|iterable $responseFactory
35+
*/
36+
public function __construct($responseFactory, string $baseUri = null)
37+
{
38+
if ($responseFactory instanceof ResponseInterface) {
39+
$responseFactory = [$responseFactory];
40+
}
41+
42+
if (!\is_callable($responseFactory) && !$responseFactory instanceof \Iterator) {
43+
$responseFactory = (function () use ($responseFactory) {
44+
yield from $responseFactory;
45+
})();
46+
}
47+
48+
$this->responseFactory = $responseFactory;
49+
$this->baseUri = $baseUri;
50+
}
51+
52+
/**
53+
* {@inheritdoc}
54+
*/
55+
public function request(string $method, string $url, array $options = []): ResponseInterface
56+
{
57+
[$url, $options] = $this->prepareRequest($method, $url, $options, ['base_uri' => $this->baseUri], true);
58+
$url = implode('', $url);
59+
60+
if (\is_callable($this->responseFactory)) {
61+
$response = ($this->responseFactory)($method, $url, $options);
62+
} elseif (!$this->responseFactory->valid()) {
63+
throw new TransportException('The response factory iterator passed to MockHttpClient is empty.');
64+
} else {
65+
$response = $this->responseFactory->current();
66+
$this->responseFactory->next();
67+
}
68+
69+
return MockResponse::fromRequest($method, $url, $options, $response);
70+
}
71+
72+
/**
73+
* {@inheritdoc}
74+
*/
75+
public function stream($responses, float $timeout = null): ResponseStreamInterface
76+
{
77+
if ($responses instanceof ResponseInterface) {
78+
$responses = [$responses];
79+
} elseif (!\is_iterable($responses)) {
80+
throw new \TypeError(sprintf('%s() expects parameter 1 to be an iterable of MockResponse objects, %s given.', __METHOD__, \is_object($responses) ? \get_class($responses) : \gettype($responses)));
81+
}
82+
83+
return new ResponseStream(MockResponse::stream($responses, $timeout));
84+
}
85+
}

0 commit comments

Comments
 (0)