replyWithFactory and replyWithCallback methods

This commit is contained in:
Pavel 2021-05-21 09:38:15 +03:00
parent 26d707000c
commit 60003ef9cc
9 changed files with 267 additions and 8 deletions

View File

@ -120,7 +120,7 @@ In order to use unsupported serializer you should create a decorator which imple
- [ ] Regexp matchers for body, query and path. - [ ] Regexp matchers for body, query and path.
- [x] Separate `UniversalMockException` into several exceptions (`PockClientException`, `PockNetworkException`, etc). - [x] Separate `UniversalMockException` into several exceptions (`PockClientException`, `PockNetworkException`, etc).
- [x] Add methods for easier throwing of exceptions listed in previous entry. - [x] Add methods for easier throwing of exceptions listed in previous entry.
- [ ] `replyCallback` - reply using specified callback. - [x] `replyWithCallback` - reply using specified callback.
- [ ] `replyFactory` - reply using specified response factory (provide corresponding interface). - [x] `replyWithFactory` - reply using specified response factory (provide corresponding interface).
- [ ] Compare XML bodies using `DOMDocument`, fallback to text comparison in case of problems. - [ ] Compare XML bodies using `DOMDocument`, fallback to text comparison in case of problems.
- [ ] Document everything (with examples if its feasible). - [ ] Document everything (with examples if its feasible).

View File

@ -81,6 +81,18 @@ class Client implements ClientInterface, HttpClient, HttpAsyncClient
return new HttpFulfilledPromise($mock->getResponse()); return new HttpFulfilledPromise($mock->getResponse());
} }
if (null !== $mock->getReplyFactory()) {
$mock->registerHit();
try {
return new HttpFulfilledPromise(
$mock->getReplyFactory()->createReply($request, new PockResponseBuilder())
);
} catch (Throwable $throwable) {
return new HttpRejectedPromise($throwable);
}
}
$throwable = $mock->getThrowable($request); $throwable = $mock->getThrowable($request);
if (null !== $throwable) { if (null !== $throwable) {
@ -94,13 +106,23 @@ class Client implements ClientInterface, HttpClient, HttpAsyncClient
} }
if (null !== $this->fallbackClient) { if (null !== $this->fallbackClient) {
try { return $this->replyWithFallbackClient($request);
return new HttpFulfilledPromise($this->fallbackClient->sendRequest($request));
} catch (Throwable $throwable) {
return new HttpRejectedPromise($throwable);
}
} }
throw new UnsupportedRequestException(); throw new UnsupportedRequestException();
} }
/**
* @param \Psr\Http\Message\RequestInterface $request
*
* @return \Http\Promise\Promise
*/
protected function replyWithFallbackClient(RequestInterface $request): Promise
{
try {
return new HttpFulfilledPromise($this->fallbackClient->sendRequest($request)); // @phpstan-ignore-line
} catch (Throwable $throwable) {
return new HttpRejectedPromise($throwable);
}
}
} }

View File

@ -0,0 +1,44 @@
<?php
/**
* PHP 7.3
*
* @category CallbackReplyFactory
* @package Pock\Factory
*/
namespace Pock\Factory;
use Pock\PockResponseBuilder;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Class CallbackReplyFactory
*
* @category CallbackReplyFactory
* @package Pock\Factory
*/
class CallbackReplyFactory implements ReplyFactoryInterface
{
/** @var callable */
private $callback;
/**
* CallbackReplyFactory constructor.
*
* @param callable $callback
*/
public function __construct(callable $callback)
{
$this->callback = $callback;
}
/**
* @inheritDoc
*/
public function createReply(RequestInterface $request, PockResponseBuilder $responseBuilder): ResponseInterface
{
return call_user_func($this->callback, $request, $responseBuilder);
}
}

View File

@ -0,0 +1,38 @@
<?php
/**
* PHP 7.3
*
* @category ReplyFactoryInterface
* @package Pock\Factory
*/
namespace Pock\Factory;
use Pock\PockResponseBuilder;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Interface ReplyFactoryInterface
*
* @category ReplyFactoryInterface
* @package Pock\Factory
*/
interface ReplyFactoryInterface
{
/**
* Reply to the specified request.
*
* If this method throws any exception, it will be treated as with the `PockBuilder::throwException call`.
*
* @see \Pock\PockBuilder::throwException()
*
* @param \Psr\Http\Message\RequestInterface $request
* @param \Pock\PockResponseBuilder $responseBuilder
*
* @return \Psr\Http\Message\ResponseInterface
* @throws \Throwable
*/
public function createReply(RequestInterface $request, PockResponseBuilder $responseBuilder): ResponseInterface;
}

View File

@ -11,6 +11,7 @@ namespace Pock;
use Pock\Exception\PockNetworkException; use Pock\Exception\PockNetworkException;
use Pock\Exception\PockRequestException; use Pock\Exception\PockRequestException;
use Pock\Factory\ReplyFactoryInterface;
use Pock\Matchers\RequestMatcherInterface; use Pock\Matchers\RequestMatcherInterface;
use Psr\Http\Client\NetworkExceptionInterface; use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Client\RequestExceptionInterface; use Psr\Http\Client\RequestExceptionInterface;
@ -29,6 +30,9 @@ class Mock implements MockInterface
/** @var \Pock\Matchers\RequestMatcherInterface */ /** @var \Pock\Matchers\RequestMatcherInterface */
private $matcher; private $matcher;
/** @var \Pock\Factory\ReplyFactoryInterface|null */
private $replyFactory;
/** @var \Psr\Http\Message\ResponseInterface|null */ /** @var \Psr\Http\Message\ResponseInterface|null */
private $response; private $response;
@ -48,6 +52,7 @@ class Mock implements MockInterface
* Mock constructor. * Mock constructor.
* *
* @param \Pock\Matchers\RequestMatcherInterface $matcher * @param \Pock\Matchers\RequestMatcherInterface $matcher
* @param \Pock\Factory\ReplyFactoryInterface|null $replyFactory
* @param \Psr\Http\Message\ResponseInterface|null $response * @param \Psr\Http\Message\ResponseInterface|null $response
* @param \Throwable|null $throwable * @param \Throwable|null $throwable
* @param int $maxHits * @param int $maxHits
@ -55,12 +60,14 @@ class Mock implements MockInterface
*/ */
public function __construct( public function __construct(
RequestMatcherInterface $matcher, RequestMatcherInterface $matcher,
?ReplyFactoryInterface $replyFactory,
?ResponseInterface $response, ?ResponseInterface $response,
?Throwable $throwable, ?Throwable $throwable,
int $maxHits, int $maxHits,
int $matchAt int $matchAt
) { ) {
$this->matcher = $matcher; $this->matcher = $matcher;
$this->replyFactory = $replyFactory;
$this->response = $response; $this->response = $response;
$this->throwable = $throwable; $this->throwable = $throwable;
$this->matchAt = $matchAt; $this->matchAt = $matchAt;
@ -128,6 +135,14 @@ class Mock implements MockInterface
return $this->response; return $this->response;
} }
/**
* @inheritDoc
*/
public function getReplyFactory(): ?ReplyFactoryInterface
{
return $this->replyFactory;
}
/** /**
* @inheritDoc * @inheritDoc
*/ */

View File

@ -9,6 +9,7 @@
namespace Pock; namespace Pock;
use Pock\Factory\ReplyFactoryInterface;
use Pock\Matchers\RequestMatcherInterface; use Pock\Matchers\RequestMatcherInterface;
use Psr\Http\Message\RequestInterface; use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
@ -53,6 +54,13 @@ interface MockInterface
*/ */
public function getResponse(): ?ResponseInterface; public function getResponse(): ?ResponseInterface;
/**
* Returns reply factory which should be used to form the mocked response.
*
* @return \Pock\Factory\ReplyFactoryInterface|null
*/
public function getReplyFactory(): ?ReplyFactoryInterface;
/** /**
* Returns the throwable which will be thrown as mock data. * Returns the throwable which will be thrown as mock data.
* *

View File

@ -15,6 +15,8 @@ use Pock\Enum\RequestScheme;
use Pock\Exception\PockClientException; use Pock\Exception\PockClientException;
use Pock\Exception\PockNetworkException; use Pock\Exception\PockNetworkException;
use Pock\Exception\PockRequestException; use Pock\Exception\PockRequestException;
use Pock\Factory\CallbackReplyFactory;
use Pock\Factory\ReplyFactoryInterface;
use Pock\Matchers\AnyRequestMatcher; use Pock\Matchers\AnyRequestMatcher;
use Pock\Matchers\BodyMatcher; use Pock\Matchers\BodyMatcher;
use Pock\Matchers\CallbackRequestMatcher; use Pock\Matchers\CallbackRequestMatcher;
@ -62,6 +64,9 @@ class PockBuilder
/** @var \Pock\PockResponseBuilder|null */ /** @var \Pock\PockResponseBuilder|null */
private $responseBuilder; private $responseBuilder;
/** @var ReplyFactoryInterface|null */
private $replyFactory;
/** @var \Throwable|null */ /** @var \Throwable|null */
private $throwable; private $throwable;
@ -454,6 +459,31 @@ class PockBuilder
return $this->responseBuilder->withStatusCode($statusCode); return $this->responseBuilder->withStatusCode($statusCode);
} }
/**
* Construct the response during request execution using provided ReplytFactoryInterface implementation.
*
* @param \Pock\Factory\ReplyFactoryInterface $factory
* @see ReplyFactoryInterface
*/
public function replyWithFactory(ReplyFactoryInterface $factory): void
{
$this->replyFactory = $factory;
}
/**
* Construct the response during request execution using provided callback.
*
* Callback should receive the same parameters as in the `ReplyFactoryInterface::createReply` method.
*
* @see ReplyFactoryInterface::createReply()
*
* @param callable $callback
*/
public function replyWithCallback(callable $callback): void
{
$this->replyWithFactory(new CallbackReplyFactory($callback));
}
/** /**
* Resets the builder. * Resets the builder.
* *
@ -462,6 +492,7 @@ class PockBuilder
public function reset(): self public function reset(): self
{ {
$this->matcher = new MultipleMatcher(); $this->matcher = new MultipleMatcher();
$this->replyFactory = null;
$this->responseBuilder = null; $this->responseBuilder = null;
$this->throwable = null; $this->throwable = null;
$this->maxHits = 1; $this->maxHits = 1;
@ -495,7 +526,7 @@ class PockBuilder
private function closePrevious(): void private function closePrevious(): void
{ {
if (null !== $this->responseBuilder || null !== $this->throwable) { if (null !== $this->responseBuilder || null !== $this->replyFactory || null !== $this->throwable) {
if (0 === count($this->matcher)) { if (0 === count($this->matcher)) {
$this->matcher->addMatcher(new AnyRequestMatcher()); $this->matcher->addMatcher(new AnyRequestMatcher());
} }
@ -508,12 +539,14 @@ class PockBuilder
$this->mocks[] = new Mock( $this->mocks[] = new Mock(
$this->matcher, $this->matcher,
$this->replyFactory,
$response, $response,
$this->throwable, $this->throwable,
$this->maxHits, $this->maxHits,
$this->matchAt $this->matchAt
); );
$this->matcher = new MultipleMatcher(); $this->matcher = new MultipleMatcher();
$this->replyFactory = null;
$this->responseBuilder = null; $this->responseBuilder = null;
$this->throwable = null; $this->throwable = null;
$this->maxHits = 1; $this->maxHits = 1;

View File

@ -12,13 +12,18 @@ namespace Pock\Tests;
use Pock\Enum\RequestMethod; use Pock\Enum\RequestMethod;
use Pock\Enum\RequestScheme; use Pock\Enum\RequestScheme;
use Pock\Exception\UnsupportedRequestException; use Pock\Exception\UnsupportedRequestException;
use Pock\Factory\ReplyFactoryInterface;
use Pock\PockBuilder; use Pock\PockBuilder;
use Pock\PockResponseBuilder;
use Pock\TestUtils\PockTestCase; use Pock\TestUtils\PockTestCase;
use Pock\TestUtils\SimpleObject; use Pock\TestUtils\SimpleObject;
use Pock\TestUtils\TestReplyFactory;
use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\NetworkExceptionInterface; use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Client\RequestExceptionInterface; use Psr\Http\Client\RequestExceptionInterface;
use Psr\Http\Message\RequestInterface; use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use RuntimeException;
/** /**
* Class PockBuilderTest * Class PockBuilderTest
@ -587,4 +592,61 @@ EOF;
self::assertEquals(1 === $i ? 200 : (4 === $i ? 201 : 400), $response->getStatusCode()); self::assertEquals(1 === $i ? 200 : (4 === $i ? 201 : 400), $response->getStatusCode());
} }
} }
public function testReplyWithFactory(): void
{
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchUri(self::TEST_URI)
->always()
->replyWithFactory(new TestReplyFactory());
for ($i = 0; $i < 5; $i++) {
$response = $builder->getClient()->sendRequest(
self::getPsr17Factory()->createRequest(RequestMethod::GET, self::TEST_URI)
);
self::assertEquals(200, $response->getStatusCode());
self::assertEquals('Request #' . ($i + 1), $response->getBody()->getContents());
}
}
public function testReplyWithCallback(): void
{
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchUri(self::TEST_URI)
->always()
->replyWithCallback(static function (RequestInterface $request, PockResponseBuilder $responseBuilder) {
return $responseBuilder->withStatusCode(200)
->withBody(self::TEST_URI)
->getResponse();
});
$response = $builder->getClient()->sendRequest(
self::getPsr17Factory()->createRequest(RequestMethod::GET, self::TEST_URI)
);
self::assertEquals(200, $response->getStatusCode());
self::assertEquals(self::TEST_URI, $response->getBody()->getContents());
}
public function testReplyWithCallbackException(): void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Exception from the callback');
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchUri(self::TEST_URI)
->always()
->replyWithCallback(static function (RequestInterface $request, PockResponseBuilder $responseBuilder) {
throw new RuntimeException('Exception from the callback');
});
$builder->getClient()->sendRequest(self::getPsr17Factory()->createRequest(
RequestMethod::GET,
self::TEST_URI
));
}
} }

View File

@ -0,0 +1,37 @@
<?php
/**
* PHP 7.3
*
* @category TestReplyFactory
* @package Pock\TestUtils
*/
namespace Pock\TestUtils;
use Pock\Factory\ReplyFactoryInterface;
use Pock\PockResponseBuilder;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Class TestReplyFactory
*
* @category TestReplyFactory
* @package Pock\TestUtils
*/
class TestReplyFactory implements ReplyFactoryInterface
{
/** @var int */
private $requestNumber = 0;
/**
* @inheritDoc
*/
public function createReply(RequestInterface $request, PockResponseBuilder $responseBuilder): ResponseInterface
{
return $responseBuilder->withStatusCode(200)
->withBody('Request #' . ++$this->requestNumber)
->getResponse();
}
}