at(N), always() methods, separate PSR-18 exceptions, throw methods for them, roadmap to stable

This commit is contained in:
Pavel 2021-05-20 20:56:45 +03:00
parent 03fc4e4c2a
commit 21021e5d70
12 changed files with 352 additions and 75 deletions

View File

@ -112,12 +112,11 @@ In order to use unsupported serializer you should create a decorator which imple
# Roadmap to stable
- [ ] `at(N)` - execute mock only at Nth call.
- [ ] `after(N)` - allow mock execution only after Nth call (for using with repeat or always).
- [ ] `always()` - always execute this mock (removes mock expiration).
- [x] `at(N)` - execute mock only at Nth call.
- [x] `always()` - always execute this mock (removes mock expiration).
- [ ] Regexp matchers for body, query and path.
- [ ] Separate `UniversalMockException` into several exceptions (`PockClientException`, `PockNetworkException`, etc).
- [ ] Add methods for easier throwing of exceptions listed in previous entry.
- [x] Separate `UniversalMockException` into several exceptions (`PockClientException`, `PockNetworkException`, etc).
- [x] Add methods for easier throwing of exceptions listed in previous entry.
- [ ] `replyCallback` - reply using specified callback.
- [ ] `replyFactory` - reply using specified response factory (provide corresponding interface).
- [ ] Compare XML bodies using `DOMDocument`, fallback to text comparison in case of problems.

View File

@ -10,7 +10,15 @@
<rule ref="rulesets/design.xml" />
<rule ref="rulesets/cleancode.xml" />
<rule ref="rulesets/codesize.xml" />
<rule ref="rulesets/naming.xml" />
<rule ref="rulesets/naming.xml">
<exclude name="ShortMethodName" />
</rule>
<rule ref="rulesets/naming.xml/ShortMethodName">
<properties>
<property name="minimum" value="2" />
</properties>
</rule>
<exclude-pattern>tests/*</exclude-pattern>
</ruleset>

View File

@ -74,17 +74,19 @@ class Client implements ClientInterface, HttpClient, HttpAsyncClient
continue;
}
if ($mock->getMatcher()->matches($request)) {
if ($mock->matches($request)) {
if (null !== $mock->getResponse()) {
$mock->registerHit();
return new HttpFulfilledPromise($mock->getResponse());
}
if (null !== $mock->getThrowable()) {
$throwable = $mock->getThrowable($request);
if (null !== $throwable) {
$mock->registerHit();
return new HttpRejectedPromise($mock->getThrowable());
return new HttpRejectedPromise($throwable);
}
throw new IncompleteMockException($mock);

View File

@ -0,0 +1,45 @@
<?php
/**
* PHP 7.3
*
* @category AbstractRequestAwareException
* @package Pock\Exception
*/
namespace Pock\Exception;
use Exception;
use Psr\Http\Message\RequestInterface;
/**
* Class AbstractRequestAwareException
*
* @category AbstractRequestAwareException
* @package Pock\Exception
*/
class AbstractRequestAwareException extends Exception
{
/** @var RequestInterface */
private $request;
/**
* @param \Psr\Http\Message\RequestInterface $request
*
* @return self
*/
public function setRequest(RequestInterface $request): self
{
$instance = new static($this->message, $this->code, $this->getPrevious()); // @phpstan-ignore-line
$instance->request = $request;
return $instance;
}
/**
* @return \Psr\Http\Message\RequestInterface
*/
public function getRequest(): RequestInterface
{
return $this->request;
}
}

View File

@ -0,0 +1,23 @@
<?php
/**
* PHP 7.3
*
* @category PockClientException
* @package Pock\Exception
*/
namespace Pock\Exception;
use Exception;
use Psr\Http\Client\ClientExceptionInterface;
/**
* Class PockClientException
*
* @category PockClientException
* @package Pock\Exception
*/
class PockClientException extends Exception implements ClientExceptionInterface
{
}

View File

@ -0,0 +1,22 @@
<?php
/**
* PHP 7.3
*
* @category PockNetworkException
* @package Pock\Exception
*/
namespace Pock\Exception;
use Psr\Http\Client\NetworkExceptionInterface;
/**
* Class PockNetworkException
*
* @category PockNetworkException
* @package Pock\Exception
*/
class PockNetworkException extends AbstractRequestAwareException implements NetworkExceptionInterface
{
}

View File

@ -0,0 +1,22 @@
<?php
/**
* PHP 7.3
*
* @category PockRequestException
* @package Pock\Exception
*/
namespace Pock\Exception;
use Psr\Http\Client\RequestExceptionInterface;
/**
* Class PockRequestException
*
* @category PockRequestException
* @package Pock\Exception
*/
class PockRequestException extends AbstractRequestAwareException implements RequestExceptionInterface
{
}

View File

@ -1,51 +0,0 @@
<?php
/**
* PHP 7.2
*
* @category UniversalMockException
* @package Pock\Exception
*/
namespace Pock\Exception;
use Exception;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Client\RequestExceptionInterface;
use Psr\Http\Message\RequestInterface;
/**
* Class UniversalMockException
*
* @category UniversalMockException
* @package Pock\Exception
*/
class UniversalMockException extends Exception implements
ClientExceptionInterface,
NetworkExceptionInterface,
RequestExceptionInterface
{
/** @var mixed */
private $request;
/**
* UniversalMockException constructor.
*
* @param mixed $request
*/
public function __construct($request)
{
parent::__construct('Default mock exception');
$this->request = $request;
}
/**
* @inheritDoc
*/
public function getRequest(): RequestInterface
{
return $this->request;
}
}

View File

@ -9,7 +9,12 @@
namespace Pock;
use Pock\Exception\PockNetworkException;
use Pock\Exception\PockRequestException;
use Pock\Matchers\RequestMatcherInterface;
use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Client\RequestExceptionInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Throwable;
@ -36,6 +41,9 @@ class Mock implements MockInterface
/** @var int */
private $maxHits;
/** @var int */
private $matchAt;
/**
* Mock constructor.
*
@ -43,18 +51,25 @@ class Mock implements MockInterface
* @param \Psr\Http\Message\ResponseInterface|null $response
* @param \Throwable|null $throwable
* @param int $maxHits
* @param int $matchAt
*/
public function __construct(
RequestMatcherInterface $matcher,
?ResponseInterface $response,
?Throwable $throwable,
int $maxHits
int $maxHits,
int $matchAt
) {
$this->matcher = $matcher;
$this->response = $response;
$this->throwable = $throwable;
$this->matchAt = $matchAt;
$this->maxHits = $maxHits;
$this->hits = 0;
if ($this->maxHits < ($matchAt + 1) && -1 !== $this->maxHits) {
$this->maxHits = $matchAt + 1;
}
}
/**
@ -62,7 +77,10 @@ class Mock implements MockInterface
*/
public function registerHit(): MockInterface
{
++$this->hits;
if (-1 !== $this->maxHits) {
++$this->hits;
}
return $this;
}
@ -71,15 +89,27 @@ class Mock implements MockInterface
*/
public function available(): bool
{
return $this->hits < $this->maxHits;
return -1 === $this->maxHits || $this->hits < $this->maxHits;
}
/**
* @inheritDoc
*/
public function getMatcher(): RequestMatcherInterface
public function matches(RequestInterface $request): bool
{
return $this->matcher;
if ($this->matcher->matches($request)) {
if ($this->matchAt <= 0) {
return true;
}
if ($this->matchAt === $this->hits) {
return true;
}
$this->registerHit();
}
return false;
}
/**
@ -101,8 +131,12 @@ class Mock implements MockInterface
/**
* @inheritDoc
*/
public function getThrowable(): ?Throwable
public function getThrowable(RequestInterface $request): ?Throwable
{
if ($this->throwable instanceof PockRequestException || $this->throwable instanceof PockNetworkException) {
return $this->throwable->setRequest($request);
}
return $this->throwable;
}
}

View File

@ -10,6 +10,7 @@
namespace Pock;
use Pock\Matchers\RequestMatcherInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Throwable;
@ -36,11 +37,14 @@ interface MockInterface
public function available(): bool;
/**
* Returns matcher for the request.
* Returns true if underlying matcher has matched provided request.
* It also returns false if matcher has matched request but hits condition is not met yet.
*
* @return \Pock\Matchers\RequestMatcherInterface
* @param \Psr\Http\Message\RequestInterface $request
*
* @return bool
*/
public function getMatcher(): RequestMatcherInterface;
public function matches(RequestInterface $request): bool;
/**
* Returns response which should be used as mock data.
@ -52,7 +56,9 @@ interface MockInterface
/**
* Returns the throwable which will be thrown as mock data.
*
* @param \Psr\Http\Message\RequestInterface $request This request may be set into exception if possible
*
* @return \Throwable|null
*/
public function getThrowable(): ?Throwable;
public function getThrowable(RequestInterface $request): ?Throwable;
}

View File

@ -12,6 +12,9 @@ namespace Pock;
use Diff\ArrayComparer\StrictArrayComparer;
use Pock\Enum\RequestMethod;
use Pock\Enum\RequestScheme;
use Pock\Exception\PockClientException;
use Pock\Exception\PockNetworkException;
use Pock\Exception\PockRequestException;
use Pock\Matchers\AnyRequestMatcher;
use Pock\Matchers\BodyMatcher;
use Pock\Matchers\CallbackRequestMatcher;
@ -45,6 +48,7 @@ use Throwable;
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @SuppressWarnings(PHPMD.TooManyPublicMethods)
* @SuppressWarnings(PHPMD.TooManyMethods)
*/
class PockBuilder
{
@ -64,6 +68,9 @@ class PockBuilder
/** @var int */
private $maxHits;
/** @var int */
private $matchAt;
/** @var \Pock\MockInterface[] */
private $mocks;
@ -329,6 +336,8 @@ class PockBuilder
*/
public function repeat(int $hits): self
{
$this->closePrevious();
if ($hits > 0) {
$this->maxHits = $hits;
}
@ -336,6 +345,49 @@ class PockBuilder
return $this;
}
/**
* Always execute this mock if matched. Mock with this call will not be expired ever.
*
* @return self
*/
public function always(): self
{
$this->closePrevious();
$this->maxHits = -1;
return $this;
}
/**
* Match request only at Nth hit. Previous matches will not be executed.
*
* **Note:** There IS a catch if you use this with the equal mocks. The test Client will not register hit
* for the second mock and the second mock will be executed at N+1 time.
*
* For example, if you try to send 5 requests with this mocks and log response codes:
* ```php
* $builder = new PockBuilder();
*
* $builder->matchHost('example.com')->at(2)->reply(200);
* $builder->matchHost('example.com')->at(4)->reply(201);
* $builder->always()->reply(400);
* ```
*
* You will get this: 400, 400, 200, 400, 400, 201
* Instead of this: 400, 400, 200, 400, 201, 400
*
* @param int $hit
*
* @return self
*/
public function at(int $hit): self
{
$this->closePrevious();
$this->matchAt = $hit - 1;
return $this;
}
/**
* Throw an exception when request is being sent.
*
@ -350,6 +402,42 @@ class PockBuilder
return $this;
}
/**
* Throw an ClientExceptionInterface instance with specified message
*
* @param string $message
*
* @return self
*/
public function throwClientException(string $message = 'Pock ClientException'): self
{
return $this->throwException(new PockClientException($message));
}
/**
* Throw an NetworkExceptionInterface instance with specified message
*
* @param string $message
*
* @return self
*/
public function throwNetworkException(string $message = 'Pock NetworkException'): self
{
return $this->throwException(new PockNetworkException($message));
}
/**
* Throw an RequestExceptionInterface instance with specified message
*
* @param string $message
*
* @return self
*/
public function throwRequestException(string $message = 'Pock RequestException'): self
{
return $this->throwException(new PockRequestException($message));
}
/**
* @param int $statusCode
*
@ -377,6 +465,7 @@ class PockBuilder
$this->responseBuilder = null;
$this->throwable = null;
$this->maxHits = 1;
$this->matchAt = -1;
$this->mocks = [];
return $this;
@ -421,12 +510,14 @@ class PockBuilder
$this->matcher,
$response,
$this->throwable,
$this->maxHits
$this->maxHits,
$this->matchAt
);
$this->matcher = new MultipleMatcher();
$this->responseBuilder = null;
$this->throwable = null;
$this->maxHits = 1;
$this->matchAt = -1;
}
}
}

View File

@ -11,14 +11,14 @@ namespace Pock\Tests;
use Pock\Enum\RequestMethod;
use Pock\Enum\RequestScheme;
use Pock\Exception\UniversalMockException;
use Pock\Exception\UnsupportedRequestException;
use Pock\PockBuilder;
use Pock\TestUtils\PockTestCase;
use Pock\TestUtils\SimpleObject;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\NetworkExceptionInterface;
use Psr\Http\Client\RequestExceptionInterface;
use Psr\Http\Message\RequestInterface;
use RuntimeException;
/**
* Class PockBuilderTest
@ -35,7 +35,7 @@ class PockBuilderTest extends PockTestCase
->createRequest(RequestMethod::GET, self::TEST_URI));
}
public function testThrowException(): void
public function testThrowClientException(): void
{
$this->expectException(ClientExceptionInterface::class);
@ -43,7 +43,37 @@ class PockBuilderTest extends PockTestCase
$builder->matchMethod(RequestMethod::GET)
->matchScheme(RequestScheme::HTTPS)
->matchHost(self::TEST_HOST)
->throwException(new UniversalMockException('Boom!'));
->throwClientException();
$builder->getClient()->sendRequest(
self::getPsr17Factory()->createRequest(RequestMethod::GET, self::TEST_URI)
);
}
public function testThrowNetworkException(): void
{
$this->expectException(NetworkExceptionInterface::class);
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchScheme(RequestScheme::HTTPS)
->matchHost(self::TEST_HOST)
->throwNetworkException();
$builder->getClient()->sendRequest(
self::getPsr17Factory()->createRequest(RequestMethod::GET, self::TEST_URI)
);
}
public function testThrowRequestException(): void
{
$this->expectException(RequestExceptionInterface::class);
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchScheme(RequestScheme::HTTPS)
->matchHost(self::TEST_HOST)
->throwRequestException();
$builder->getClient()->sendRequest(
self::getPsr17Factory()->createRequest(RequestMethod::GET, self::TEST_URI)
@ -511,4 +541,50 @@ EOF;
);
self::assertEquals('Second token (post)', $response->getBody()->getContents());
}
public function testAlways(): void
{
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchUri(self::TEST_URI)
->always()
->reply(200)
->withHeader('Content-Type', 'text/plain')
->withBody('Successful');
for ($i = 0; $i < 10; $i++) {
$response = $builder->getClient()->sendRequest(
self::getPsr17Factory()->createRequest(RequestMethod::GET, self::TEST_URI)
);
self::assertEquals(200, $response->getStatusCode());
self::assertEquals(['Content-Type' => ['text/plain']], $response->getHeaders());
self::assertEquals('Successful', $response->getBody()->getContents());
}
}
public function testAt(): void
{
$builder = new PockBuilder();
$builder->matchMethod(RequestMethod::GET)
->matchUri(self::TEST_URI)
->at(2)
->reply(200);
$builder->matchMethod(RequestMethod::GET)
->matchUri(self::TEST_URI)
->at(4)
->reply(201);
$builder->always()->reply(400);
$builder->getClient();
for ($i = 0; $i < 5; $i++) {
$response = $builder->getClient()->sendRequest(
self::getPsr17Factory()->createRequest(RequestMethod::GET, self::TEST_URI)
);
self::assertEquals(1 === $i ? 200 : (4 === $i ? 201 : 400), $response->getStatusCode());
}
}
}