-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[HttpClient] Add a mock client #30553
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
651e77d
[HttpClient] Add a mock client
GaryPEGEOT d0fec53
Simplify usage
GaryPEGEOT a23d93b
Test when things goes wrong
GaryPEGEOT 2cd81de
Fix stream behavior, add error test cases.
GaryPEGEOT 206d786
Remove useless method
GaryPEGEOT 72aa094
add a mock response
GaryPEGEOT b3d4a28
1st round fixes
GaryPEGEOT e2c69c2
Remove useless try
GaryPEGEOT 9210c1d
Typo fix
GaryPEGEOT 5985f9c
Switch to private
GaryPEGEOT File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\HttpClient; | ||
|
||
use Symfony\Component\HttpClient\Chunk\DataChunk; | ||
use Symfony\Component\HttpClient\Chunk\ErrorChunk; | ||
use Symfony\Component\HttpClient\Chunk\FirstChunk; | ||
use Symfony\Component\HttpClient\Chunk\LastChunk; | ||
use Symfony\Component\HttpClient\Exception\TransportException; | ||
use Symfony\Component\HttpClient\Response\ResponseStream; | ||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; | ||
use Symfony\Contracts\HttpClient\HttpClientInterface; | ||
use Symfony\Contracts\HttpClient\ResponseInterface; | ||
use Symfony\Contracts\HttpClient\ResponseStreamInterface; | ||
|
||
/** | ||
* Provides a way to tests the HttpClient without making actual HTTP requests. | ||
* | ||
* @author Gary PEGEOT <garypegeot@gmail.com> | ||
*/ | ||
class MockClient implements HttpClientInterface | ||
{ | ||
/** | ||
* Predefined responses. Throw a TransportExceptionInterface when none provided. | ||
* | ||
* @var ResponseInterface[] | ||
*/ | ||
private $responses = []; | ||
|
||
/** | ||
* MockClient constructor. | ||
* | ||
* @param iterable|ResponseInterface[] $responses | ||
*/ | ||
public function __construct(iterable $responses = []) | ||
{ | ||
foreach ($responses as $response) { | ||
if (!$response instanceof ResponseInterface) { | ||
throw new \TypeError(sprintf('Each predefined response must an instance of %s, %s given.', ResponseInterface::class, \is_object($response) ? \get_class($response) : \gettype($response))); | ||
} | ||
|
||
$this->responses[] = $response; | ||
} | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function request(string $method, string $url, array $options = []): ResponseInterface | ||
{ | ||
if (!\count($this->responses)) { | ||
throw new TransportException('No predefined response to send. Please add one or more using "addResponse" method.'); | ||
} | ||
|
||
return \array_shift($this->responses); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function stream($responses, float $timeout = null): ResponseStreamInterface | ||
{ | ||
if ($responses instanceof ResponseInterface) { | ||
$responses = [$responses]; | ||
} elseif (!\is_iterable($responses)) { | ||
throw new \TypeError(sprintf('%s() expects parameter 1 to be an iterable of ResponseInterface objects, %s given.', __METHOD__, \is_object($responses) ? \get_class($responses) : \gettype($responses))); | ||
} | ||
|
||
return new ResponseStream($this->streamResponses($responses)); | ||
} | ||
|
||
/** | ||
* @return $this | ||
*/ | ||
public function addResponse(ResponseInterface $response) | ||
{ | ||
$this->responses[] = $response; | ||
|
||
return $this; | ||
} | ||
|
||
/** | ||
* Clears all predefined responses. | ||
*/ | ||
public function clear(): void | ||
{ | ||
$this->responses = []; | ||
} | ||
|
||
private function streamResponses(iterable $responses): \Generator | ||
{ | ||
foreach ($responses as $response) { | ||
try { | ||
$response->getHeaders(true); | ||
|
||
yield $response => new FirstChunk(); | ||
yield $response => new DataChunk(0, $content = $response->getContent(true)); | ||
yield $response => new LastChunk(\strlen($content)); | ||
} catch (TransportExceptionInterface $e) { | ||
$didThrow = false; | ||
|
||
yield $response => new ErrorChunk($didThrow, 0, $e); | ||
} | ||
} | ||
} | ||
} |
107 changes: 107 additions & 0 deletions
107
src/Symfony/Component/HttpClient/Response/MockResponse.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\HttpClient\Response; | ||
|
||
use Symfony\Contracts\HttpClient\ResponseInterface; | ||
|
||
/** | ||
* A test-friendly response. | ||
* | ||
* @author Gary PEGEOT <garypegeot@gmail.com> | ||
*/ | ||
class MockResponse implements ResponseInterface | ||
{ | ||
use ResponseTrait; | ||
|
||
private const DEFAULT_INFO = [ | ||
'raw_headers' => [], // An array modelled after the special $http_response_header variable | ||
'redirect_count' => 0, // The number of redirects followed while executing the request | ||
'redirect_url' => null, // The resolved location of redirect responses, null otherwise | ||
'start_time' => 0.0, // The time when the request was sent or 0.0 when it's pending | ||
'http_method' => 'GET', // The HTTP verb of the last request | ||
'http_code' => 0, // The last response code or 0 when it is not known yet | ||
'error' => null, // The error message when the transfer was aborted, null otherwise | ||
'data' => null, // The value of the "data" request option, null if not set | ||
'url' => '', // The last effective URL of the request | ||
]; | ||
/** | ||
* @var callable|null | ||
*/ | ||
private $initializer; | ||
|
||
public function __construct(string $content = '', int $code = 200, array $headers = [], array $info = [], ?callable $initializer = null) | ||
{ | ||
$default = self::DEFAULT_INFO; | ||
$default['start_time'] = microtime(true); | ||
|
||
$this->content = $content; | ||
$this->info = \array_merge($default, $info); | ||
$this->info['http_code'] = $code; | ||
$this->initializer = $initializer; | ||
$this->headers = $headers; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getInfo(string $type = null) | ||
{ | ||
if ($type) { | ||
return $this->info[$type] ?? null; | ||
} | ||
|
||
return $this->info; | ||
} | ||
|
||
public function getContent(bool $throw = true): string | ||
{ | ||
if ($this->initializer) { | ||
($this->initializer)($this); | ||
$this->initializer = null; | ||
} | ||
|
||
if ($throw) { | ||
$this->checkStatusCode(); | ||
} | ||
|
||
return $this->content; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
protected function close(): void | ||
{ | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
protected static function schedule(self $response, array &$runningResponses): void | ||
{ | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
protected static function perform(\stdClass $multi, array &$responses): void | ||
{ | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
protected static function select(\stdClass $multi, float $timeout): int | ||
{ | ||
return 42; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\HttpClient\Tests; | ||
|
||
use PHPUnit\Framework\TestCase; | ||
use Symfony\Component\HttpClient\Exception\TransportException; | ||
use Symfony\Component\HttpClient\MockClient; | ||
use Symfony\Component\HttpClient\Response\MockResponse; | ||
use Symfony\Contracts\HttpClient\ChunkInterface; | ||
use Symfony\Contracts\HttpClient\ResponseInterface; | ||
|
||
class MockClientTest extends TestCase | ||
{ | ||
public function testStream() | ||
{ | ||
$response = new MockResponse('{"foo": "bar"}'); | ||
|
||
$client = new MockClient(); | ||
$chunks = ''; | ||
|
||
foreach ($client->stream($response) as $chunk) { | ||
$this->assertInstanceOf(ChunkInterface::class, $chunk); | ||
|
||
$chunks .= $chunk->getContent(); | ||
} | ||
|
||
$this->assertSame('{"foo": "bar"}', $chunks); | ||
} | ||
|
||
public function testStreamWithUnhappyResponse() | ||
{ | ||
$client = new MockClient(); | ||
$response = $this->createMock(ResponseInterface::class); | ||
$e = new TransportException('Something is broken :('); | ||
|
||
$response->method('getHeaders')->willThrowException($e); | ||
|
||
$this->expectException(TransportException::class); | ||
$this->expectExceptionMessage($e->getMessage()); | ||
|
||
$client->stream($response)->valid(); | ||
} | ||
|
||
public function testRequest() | ||
{ | ||
/** @var ResponseInterface $response */ | ||
$response = new MockResponse(); | ||
$client = new MockClient(); | ||
$client->addResponse($response); | ||
|
||
$this->assertSame($response, $client->request('GET', '/whatever?q=foo', ['base_uri' => 'http://example.org'])); | ||
} | ||
|
||
public function testRequestWithoutResponse() | ||
{ | ||
$this->expectException(TransportException::class); | ||
$this->expectExceptionMessage('No predefined response to send. Please add one or more using "addResponse" method.'); | ||
|
||
(new MockClient())->request('GET', '/whatever?q=foo', ['base_uri' => 'http://example.org']); | ||
} | ||
|
||
public function testConstructWithInvalidType() | ||
{ | ||
$this->expectException('TypeError'); | ||
$this->expectExceptionMessage('Each predefined response must an instance of Symfony\Contracts\HttpClient\ResponseInterface, stdClass given.'); | ||
|
||
new MockClient([new \stdClass()]); | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.