mirror of
https://gitee.com/hyperf/hyperf.git
synced 2024-11-29 18:27:44 +08:00
⚙️chore(testing) move container related methods to InteractsWithContainer trait
New TestCase 🛠️refactor(testing) remove duplicate ApplicationContext::setContainer call ⚙️chore(testing) add array access to TestResponse 🛠️refactor(testing) simplify TestResponse body implementation 🚀feat(response) add status and content methods to TestResponse class Adds Utils Remove utils Update TestResponse.php ⚙️chore(testing) refreshContainer with ApplicationInterface call 🛠️refactor(testing): fix namespace in TestCase.php 🛠️refactor(testing) update namespace for ApplicationInterface in refreshContainer method ⚙️chore(testing) ignore phpstan warning in InteractsWithContainer trait instance function 🛠️refactor(testing): make container property protected in TestCase class 🛠️refactor(response) refactor TestResponse class methods - Refactored the content method to use the getContent method for consistency. - Created a collect method to get the JSON decoded body of the response as a collection for convenience. - Removed the status method since it's redundant with the getStatusCode method. - Updated the assertContent method to use the getContent method instead of the content method. 🛠️refactor(testing) make assert methods chainable Adds Utils WIP 🚀feat(dependencies) add symfony/http-foundation\n- Added version constraints for symfony/http-foundation for compatibility with versions 5.4 and 6.0. 🛠️refactor(tests) update TestCase.php null initialization of container ⚙️chore(config) update phpstan.neon configuration file
This commit is contained in:
parent
eba329e1c2
commit
ce516ea2a3
@ -83,6 +83,7 @@
|
||||
"symfony/console": "^5.0|^6.0",
|
||||
"symfony/event-dispatcher": "^5.0|^6.0",
|
||||
"symfony/finder": "^5.0|^6.0",
|
||||
"symfony/http-foundation": "^5.4|^6.0",
|
||||
"symfony/property-access": "^5.0|^6.0",
|
||||
"symfony/serializer": "^5.0|^6.0",
|
||||
"symfony/uid": "^5.0|^6.0",
|
||||
|
@ -69,3 +69,4 @@ parameters:
|
||||
- '#\(Hyperf\\Utils\\Collection\) does not accept Hyperf\\Collection\\Collection#'
|
||||
- '#should return Hyperf\\Utils\\Collection but returns Hyperf\\Collection\\Collectio#'
|
||||
- '#Call to an undefined method Hyperf\\Collection\\HigherOrderCollectionProxy::\w+\(\)#'
|
||||
- '#Unreachable statement \- code above always terminates#'
|
||||
|
@ -29,7 +29,8 @@
|
||||
"hyperf/http-message": "~3.0.0",
|
||||
"hyperf/http-server": "~3.0.0",
|
||||
"hyperf/support": "~3.0.0",
|
||||
"hyperf/utils": "~3.0.0"
|
||||
"hyperf/utils": "~3.0.0",
|
||||
"symfony/http-foundation": "^5.4|^6.0"
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
|
44
src/testing/src/Assert.php
Normal file
44
src/testing/src/Assert.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
namespace Hyperf\Testing;
|
||||
|
||||
use ArrayAccess;
|
||||
use Hyperf\Testing\Constraint\ArraySubset;
|
||||
use Hyperf\Testing\Exception\InvalidArgumentException;
|
||||
use PHPUnit\Framework\Assert as PHPUnit;
|
||||
|
||||
/**
|
||||
* @internal this class is not meant to be used or overwritten outside the framework itself
|
||||
*/
|
||||
abstract class Assert extends PHPUnit
|
||||
{
|
||||
/**
|
||||
* Asserts that an array has a specified subset.
|
||||
*
|
||||
* @param array|ArrayAccess $subset
|
||||
* @param array|ArrayAccess $array
|
||||
*/
|
||||
public static function assertArraySubset($subset, $array, bool $checkForIdentity = false, string $msg = ''): void
|
||||
{
|
||||
if (! (is_array($subset) || $subset instanceof ArrayAccess)) {
|
||||
throw InvalidArgumentException::create(1, 'array or ArrayAccess');
|
||||
}
|
||||
|
||||
if (! (is_array($array) || $array instanceof ArrayAccess)) {
|
||||
throw InvalidArgumentException::create(2, 'array or ArrayAccess');
|
||||
}
|
||||
|
||||
$constraint = new ArraySubset($subset, $checkForIdentity);
|
||||
|
||||
PHPUnit::assertThat($array, $constraint, $msg);
|
||||
}
|
||||
}
|
408
src/testing/src/AssertableJsonString.php
Normal file
408
src/testing/src/AssertableJsonString.php
Normal file
@ -0,0 +1,408 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
namespace Hyperf\Testing;
|
||||
|
||||
use ArrayAccess;
|
||||
use Closure;
|
||||
use Countable;
|
||||
use Hyperf\Collection\Arr;
|
||||
use Hyperf\Contract\Jsonable;
|
||||
use Hyperf\Stringable\Str;
|
||||
use Hyperf\Testing\Assert as PHPUnit;
|
||||
use JsonSerializable;
|
||||
|
||||
class AssertableJsonString implements ArrayAccess, Countable
|
||||
{
|
||||
/**
|
||||
* The original encoded json.
|
||||
*
|
||||
* @var array|Jsonable|JsonSerializable|string
|
||||
*/
|
||||
public $json;
|
||||
|
||||
/**
|
||||
* The decoded json contents.
|
||||
*
|
||||
* @var null|array
|
||||
*/
|
||||
protected $decoded;
|
||||
|
||||
/**
|
||||
* Create a new assertable JSON string instance.
|
||||
*
|
||||
* @param array|Jsonable|JsonSerializable|string $jsonable
|
||||
*/
|
||||
public function __construct($jsonable)
|
||||
{
|
||||
$this->json = $jsonable;
|
||||
|
||||
if ($jsonable instanceof JsonSerializable) {
|
||||
$this->decoded = $jsonable->jsonSerialize();
|
||||
} elseif ($jsonable instanceof Jsonable) {
|
||||
$this->decoded = json_decode($jsonable->__toString(), true);
|
||||
} elseif (is_array($jsonable)) {
|
||||
$this->decoded = $jsonable;
|
||||
} else {
|
||||
$this->decoded = json_decode($jsonable, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and return the decoded response JSON.
|
||||
*
|
||||
* @param null|string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function json($key = null)
|
||||
{
|
||||
return data_get($this->decoded, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response JSON has the expected count of items at the given key.
|
||||
*
|
||||
* @param null|string $key
|
||||
* @return $this
|
||||
*/
|
||||
public function assertCount(int $count, $key = null)
|
||||
{
|
||||
if (! is_null($key)) {
|
||||
PHPUnit::assertCount(
|
||||
$count,
|
||||
data_get($this->decoded, $key),
|
||||
"Failed to assert that the response count matched the expected {$count}"
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
PHPUnit::assertCount(
|
||||
$count,
|
||||
$this->decoded,
|
||||
"Failed to assert that the response count matched the expected {$count}"
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has the exact given JSON.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertExact(array $data)
|
||||
{
|
||||
$actual = $this->reorderAssocKeys((array) $this->decoded);
|
||||
|
||||
$expected = $this->reorderAssocKeys($data);
|
||||
|
||||
PHPUnit::assertEquals(
|
||||
json_encode($expected, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
|
||||
json_encode($actual, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has the similar JSON as given.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertSimilar(array $data)
|
||||
{
|
||||
$actual = json_encode(
|
||||
Arr::sortRecursive((array) $this->decoded),
|
||||
JSON_UNESCAPED_UNICODE
|
||||
);
|
||||
|
||||
PHPUnit::assertEquals(json_encode(Arr::sortRecursive($data), JSON_UNESCAPED_UNICODE), $actual);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response contains the given JSON fragment.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertFragment(array $data)
|
||||
{
|
||||
$actual = json_encode(
|
||||
Arr::sortRecursive((array) $this->decoded),
|
||||
JSON_UNESCAPED_UNICODE
|
||||
);
|
||||
|
||||
foreach (Arr::sortRecursive($data) as $key => $value) {
|
||||
$expected = $this->jsonSearchStrings($key, $value);
|
||||
|
||||
PHPUnit::assertTrue(
|
||||
Str::contains($actual, $expected),
|
||||
'Unable to find JSON fragment: ' . PHP_EOL . PHP_EOL .
|
||||
'[' . json_encode([$key => $value], JSON_UNESCAPED_UNICODE) . ']' . PHP_EOL . PHP_EOL .
|
||||
'within' . PHP_EOL . PHP_EOL .
|
||||
"[{$actual}]."
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response does not contain the given JSON fragment.
|
||||
*
|
||||
* @param bool $exact
|
||||
* @return $this
|
||||
*/
|
||||
public function assertMissing(array $data, $exact = false)
|
||||
{
|
||||
if ($exact) {
|
||||
return $this->assertMissingExact($data);
|
||||
}
|
||||
|
||||
$actual = json_encode(
|
||||
Arr::sortRecursive((array) $this->decoded),
|
||||
JSON_UNESCAPED_UNICODE
|
||||
);
|
||||
|
||||
foreach (Arr::sortRecursive($data) as $key => $value) {
|
||||
$unexpected = $this->jsonSearchStrings($key, $value);
|
||||
|
||||
PHPUnit::assertFalse(
|
||||
Str::contains($actual, $unexpected),
|
||||
'Found unexpected JSON fragment: ' . PHP_EOL . PHP_EOL .
|
||||
'[' . json_encode([$key => $value], JSON_UNESCAPED_UNICODE) . ']' . PHP_EOL . PHP_EOL .
|
||||
'within' . PHP_EOL . PHP_EOL .
|
||||
"[{$actual}]."
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response does not contain the exact JSON fragment.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertMissingExact(array $data)
|
||||
{
|
||||
$actual = json_encode(
|
||||
Arr::sortRecursive((array) $this->decoded),
|
||||
JSON_UNESCAPED_UNICODE
|
||||
);
|
||||
|
||||
foreach (Arr::sortRecursive($data) as $key => $value) {
|
||||
$unexpected = $this->jsonSearchStrings($key, $value);
|
||||
|
||||
if (! Str::contains($actual, $unexpected)) {
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
PHPUnit::fail(
|
||||
'Found unexpected JSON fragment: ' . PHP_EOL . PHP_EOL .
|
||||
'[' . json_encode($data, JSON_UNESCAPED_UNICODE) . ']' . PHP_EOL . PHP_EOL .
|
||||
'within' . PHP_EOL . PHP_EOL .
|
||||
"[{$actual}]."
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response does not contain the given path.
|
||||
*
|
||||
* @param string $path
|
||||
* @return $this
|
||||
*/
|
||||
public function assertMissingPath($path)
|
||||
{
|
||||
PHPUnit::assertFalse(Arr::has($this->json(), $path));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the expected value and type exists at the given path in the response.
|
||||
*
|
||||
* @param string $path
|
||||
* @param mixed $expect
|
||||
* @return $this
|
||||
*/
|
||||
public function assertPath($path, $expect)
|
||||
{
|
||||
if ($expect instanceof Closure) {
|
||||
PHPUnit::assertTrue($expect($this->json($path)));
|
||||
} else {
|
||||
PHPUnit::assertSame($expect, $this->json($path));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has a given JSON structure.
|
||||
*
|
||||
* @param null|array $responseData
|
||||
* @return $this
|
||||
*/
|
||||
public function assertStructure(array $structure = null, $responseData = null)
|
||||
{
|
||||
if (is_null($structure)) {
|
||||
return $this->assertSimilar($this->decoded);
|
||||
}
|
||||
|
||||
if (! is_null($responseData)) {
|
||||
return (new static($responseData))->assertStructure($structure);
|
||||
}
|
||||
|
||||
foreach ($structure as $key => $value) {
|
||||
if (is_array($value) && $key === '*') {
|
||||
PHPUnit::assertIsArray($this->decoded);
|
||||
|
||||
foreach ($this->decoded as $responseDataItem) {
|
||||
$this->assertStructure($structure['*'], $responseDataItem);
|
||||
}
|
||||
} elseif (is_array($value)) {
|
||||
PHPUnit::assertArrayHasKey($key, $this->decoded);
|
||||
|
||||
$this->assertStructure($structure[$key], $this->decoded[$key]);
|
||||
} else {
|
||||
PHPUnit::assertArrayHasKey($value, $this->decoded);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response is a superset of the given JSON.
|
||||
*
|
||||
* @param bool $strict
|
||||
* @return $this
|
||||
*/
|
||||
public function assertSubset(array $data, $strict = false)
|
||||
{
|
||||
PHPUnit::assertArraySubset(
|
||||
$data,
|
||||
$this->decoded,
|
||||
$strict,
|
||||
$this->assertJsonMessage($data)
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of items in the underlying JSON array.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->decoded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether an offset exists.
|
||||
*
|
||||
* @param mixed $offset
|
||||
*/
|
||||
public function offsetExists($offset): bool
|
||||
{
|
||||
return isset($this->decoded[$offset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the given offset.
|
||||
*
|
||||
* @param string $offset
|
||||
*/
|
||||
public function offsetGet($offset): mixed
|
||||
{
|
||||
return $this->decoded[$offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value at the given offset.
|
||||
*
|
||||
* @param string $offset
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function offsetSet($offset, $value): void
|
||||
{
|
||||
$this->decoded[$offset] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset the value at the given offset.
|
||||
*
|
||||
* @param string $offset
|
||||
*/
|
||||
public function offsetUnset($offset): void
|
||||
{
|
||||
unset($this->decoded[$offset]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder associative array keys to make it easy to compare arrays.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function reorderAssocKeys(array $data)
|
||||
{
|
||||
$data = Arr::dot($data);
|
||||
ksort($data);
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
Arr::set($result, $key, $value);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the assertion message for assertJson.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function assertJsonMessage(array $data)
|
||||
{
|
||||
$expected = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$actual = json_encode($this->decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
return 'Unable to find JSON: ' . PHP_EOL . PHP_EOL .
|
||||
"[{$expected}]" . PHP_EOL . PHP_EOL .
|
||||
'within response JSON:' . PHP_EOL . PHP_EOL .
|
||||
"[{$actual}]." . PHP_EOL . PHP_EOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the strings we need to search for when examining the JSON.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
* @return array
|
||||
*/
|
||||
protected function jsonSearchStrings($key, $value)
|
||||
{
|
||||
$needle = Str::substr(json_encode([$key => $value], JSON_UNESCAPED_UNICODE), 1, -1);
|
||||
|
||||
return [
|
||||
$needle . ']',
|
||||
$needle . '}',
|
||||
$needle . ',',
|
||||
];
|
||||
}
|
||||
}
|
@ -36,6 +36,9 @@ use Throwable;
|
||||
use function Hyperf\Collection\data_get;
|
||||
use function Hyperf\Coroutine\wait;
|
||||
|
||||
/**
|
||||
* @deprecated since 3.1
|
||||
*/
|
||||
class Client extends Server
|
||||
{
|
||||
protected PackerInterface $packer;
|
||||
|
95
src/testing/src/Concerns/InteractsWithContainer.php
Normal file
95
src/testing/src/Concerns/InteractsWithContainer.php
Normal file
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
namespace Hyperf\Testing\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Hyperf\Context\ApplicationContext;
|
||||
use Hyperf\Di\Container;
|
||||
use Hyperf\Di\Definition\DefinitionSourceFactory;
|
||||
use Mockery;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
trait InteractsWithContainer
|
||||
{
|
||||
protected ?ContainerInterface $container = null;
|
||||
|
||||
/**
|
||||
* Register an instance of an object in the container.
|
||||
*
|
||||
* @param string $abstract
|
||||
* @param object $instance
|
||||
* @return object
|
||||
*/
|
||||
protected function swap($abstract, $instance)
|
||||
{
|
||||
return $this->instance($abstract, $instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an instance of an object in the container.
|
||||
*
|
||||
* @param string $abstract
|
||||
* @param object $instance
|
||||
* @return object
|
||||
*/
|
||||
protected function instance($abstract, $instance)
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
$this->container->set($abstract, $instance);
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock an instance of an object in the container.
|
||||
*
|
||||
* @param string $abstract
|
||||
* @return \Mockery\MockInterface
|
||||
*/
|
||||
protected function mock($abstract, Closure $mock = null)
|
||||
{
|
||||
return $this->instance($abstract, Mockery::mock(...array_filter(func_get_args())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock a partial instance of an object in the container.
|
||||
*
|
||||
* @param string $abstract
|
||||
* @return \Mockery\MockInterface
|
||||
*/
|
||||
protected function partialMock($abstract, Closure $mock = null)
|
||||
{
|
||||
return $this->instance($abstract, Mockery::mock(...array_filter(func_get_args()))->makePartial());
|
||||
}
|
||||
|
||||
/**
|
||||
* Spy an instance of an object in the container.
|
||||
*
|
||||
* @param string $abstract
|
||||
* @return \Mockery\MockInterface
|
||||
*/
|
||||
protected function spy($abstract, Closure $mock = null)
|
||||
{
|
||||
return $this->instance($abstract, Mockery::spy(...array_filter(func_get_args())));
|
||||
}
|
||||
|
||||
protected function refreshContainer(): void
|
||||
{
|
||||
$this->container = ApplicationContext::setContainer($this->createContainer());
|
||||
$this->container->get(\Hyperf\Contract\ApplicationInterface::class);
|
||||
}
|
||||
|
||||
protected function createContainer(): ContainerInterface
|
||||
{
|
||||
return new Container((new DefinitionSourceFactory())());
|
||||
}
|
||||
}
|
62
src/testing/src/Concerns/MakesHttpRequests.php
Normal file
62
src/testing/src/Concerns/MakesHttpRequests.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
namespace Hyperf\Testing\Concerns;
|
||||
|
||||
use Hyperf\Testing\Http\Client;
|
||||
use Hyperf\Testing\Http\TestResponse;
|
||||
|
||||
use function Hyperf\Support\make;
|
||||
|
||||
trait MakesHttpRequests
|
||||
{
|
||||
protected function get($uri, array $data = [], array $headers = []): TestResponse
|
||||
{
|
||||
return $this->doRequest(__FUNCTION__, $uri, $data, $headers);
|
||||
}
|
||||
|
||||
protected function post($uri, array $data = [], array $headers = []): TestResponse
|
||||
{
|
||||
return $this->doRequest(__FUNCTION__, $uri, $data, $headers);
|
||||
}
|
||||
|
||||
protected function put($uri, array $data = [], array $headers = []): TestResponse
|
||||
{
|
||||
return $this->doRequest(__FUNCTION__, $uri, $data, $headers);
|
||||
}
|
||||
|
||||
protected function delete($uri, array $data = [], array $headers = []): TestResponse
|
||||
{
|
||||
return $this->doRequest(__FUNCTION__, $uri, $data, $headers);
|
||||
}
|
||||
|
||||
protected function json($uri, array $data = [], array $headers = []): TestResponse
|
||||
{
|
||||
return $this->doRequest(__FUNCTION__, $uri, $data, $headers);
|
||||
}
|
||||
|
||||
protected function file($uri, array $data = [], array $headers = []): TestResponse
|
||||
{
|
||||
return $this->doRequest(__FUNCTION__, $uri, $data, $headers);
|
||||
}
|
||||
|
||||
protected function doRequest(string $method, ...$args): TestResponse
|
||||
{
|
||||
return $this->createTestResponse(
|
||||
make(Client::class)->{$method}(...$args)
|
||||
);
|
||||
}
|
||||
|
||||
protected function createTestResponse($response): TestResponse
|
||||
{
|
||||
return new TestResponse($response);
|
||||
}
|
||||
}
|
139
src/testing/src/Constraint/ArraySubset.php
Normal file
139
src/testing/src/Constraint/ArraySubset.php
Normal file
@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
namespace Hyperf\Testing\Constraint;
|
||||
|
||||
use ArrayObject;
|
||||
use PHPUnit\Framework\Constraint\Constraint;
|
||||
use SebastianBergmann\Comparator\ComparisonFailure;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* @internal this class is not meant to be used or overwritten outside the framework itself
|
||||
*/
|
||||
final class ArraySubset extends Constraint
|
||||
{
|
||||
/**
|
||||
* @var iterable
|
||||
*/
|
||||
private $subset;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $strict;
|
||||
|
||||
/**
|
||||
* Create a new array subset constraint instance.
|
||||
*/
|
||||
public function __construct(iterable $subset, bool $strict = false)
|
||||
{
|
||||
$this->strict = $strict;
|
||||
$this->subset = $subset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the constraint for parameter $other.
|
||||
*
|
||||
* If $returnResult is set to false (the default), an exception is thrown
|
||||
* in case of a failure. null is returned otherwise.
|
||||
*
|
||||
* If $returnResult is true, the result of the evaluation is returned as
|
||||
* a boolean value instead: true in case of success, false in case of a
|
||||
* failure.
|
||||
*
|
||||
* @param mixed $other
|
||||
*
|
||||
* @throws \PHPUnit\Framework\ExpectationFailedException
|
||||
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
|
||||
*/
|
||||
public function evaluate($other, string $description = '', bool $returnResult = false): ?bool
|
||||
{
|
||||
// type cast $other & $this->subset as an array to allow
|
||||
// support in standard array functions.
|
||||
$other = $this->toArray($other);
|
||||
$this->subset = $this->toArray($this->subset);
|
||||
|
||||
$patched = array_replace_recursive($other, $this->subset);
|
||||
|
||||
if ($this->strict) {
|
||||
$result = $other === $patched;
|
||||
} else {
|
||||
$result = $other == $patched;
|
||||
}
|
||||
|
||||
if ($returnResult) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if (! $result) {
|
||||
$f = new ComparisonFailure(
|
||||
$patched,
|
||||
$other,
|
||||
var_export($patched, true),
|
||||
var_export($other, true)
|
||||
);
|
||||
|
||||
$this->fail($other, $description, $f);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the constraint.
|
||||
*
|
||||
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return 'has the subset ' . $this->exporter()->export($this->subset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the description of the failure.
|
||||
*
|
||||
* The beginning of failure messages is "Failed asserting that" in most
|
||||
* cases. This method should return the second part of that sentence.
|
||||
*
|
||||
* @param mixed $other
|
||||
*
|
||||
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
|
||||
*/
|
||||
protected function failureDescription($other): string
|
||||
{
|
||||
return 'an array ' . $this->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the description of the failure.
|
||||
*
|
||||
* The beginning of failure messages is "Failed asserting that" in most
|
||||
* cases. This method should return the second part of that sentence.
|
||||
*/
|
||||
private function toArray(iterable $other): array
|
||||
{
|
||||
if (is_array($other)) {
|
||||
return $other;
|
||||
}
|
||||
|
||||
if ($other instanceof ArrayObject) {
|
||||
return $other->getArrayCopy();
|
||||
}
|
||||
|
||||
if ($other instanceof Traversable) {
|
||||
return iterator_to_array($other);
|
||||
}
|
||||
|
||||
// Keep BC even if we know that array would not be the expected one
|
||||
return (array) $other;
|
||||
}
|
||||
}
|
92
src/testing/src/Constraint/SeeInOrder.php
Normal file
92
src/testing/src/Constraint/SeeInOrder.php
Normal file
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
namespace Hyperf\Testing\Constraint;
|
||||
|
||||
use PHPUnit\Framework\Constraint\Constraint;
|
||||
use ReflectionClass;
|
||||
|
||||
class SeeInOrder extends Constraint
|
||||
{
|
||||
/**
|
||||
* The string under validation.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* The last value that failed to pass validation.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $failedValue;
|
||||
|
||||
/**
|
||||
* Create a new constraint instance.
|
||||
*
|
||||
* @param string $content
|
||||
*/
|
||||
public function __construct($content)
|
||||
{
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the rule passes validation.
|
||||
*
|
||||
* @param array $values
|
||||
*/
|
||||
public function matches($values): bool
|
||||
{
|
||||
$position = 0;
|
||||
|
||||
foreach ($values as $value) {
|
||||
if (empty($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$valuePosition = mb_strpos($this->content, $value, $position);
|
||||
|
||||
if ($valuePosition === false || $valuePosition < $position) {
|
||||
$this->failedValue = $value;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$position = $valuePosition + mb_strlen($value);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the description of the failure.
|
||||
*
|
||||
* @param array $values
|
||||
*/
|
||||
public function failureDescription($values): string
|
||||
{
|
||||
return sprintf(
|
||||
'Failed asserting that \'%s\' contains "%s" in specified order.',
|
||||
$this->content,
|
||||
$this->failedValue
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a string representation of the object.
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return (new ReflectionClass($this))->name;
|
||||
}
|
||||
}
|
41
src/testing/src/Exception/InvalidArgumentException.php
Normal file
41
src/testing/src/Exception/InvalidArgumentException.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
namespace Hyperf\Testing\Exception;
|
||||
|
||||
use PHPUnit\Framework\Exception;
|
||||
|
||||
class InvalidArgumentException extends Exception
|
||||
{
|
||||
/**
|
||||
* Creates a new exception for an invalid argument.
|
||||
*/
|
||||
public static function create(int $argument, string $type): static
|
||||
{
|
||||
$stack = debug_backtrace();
|
||||
|
||||
$function = $stack[1]['function'];
|
||||
|
||||
if (isset($stack[1]['class'])) {
|
||||
$function = sprintf('%s::%s', $stack[1]['class'], $stack[1]['function']);
|
||||
}
|
||||
|
||||
return new static(
|
||||
sprintf(
|
||||
'Argument #%d of %s() must be %s %s',
|
||||
$argument,
|
||||
$function,
|
||||
in_array(lcfirst($type)[0], ['a', 'e', 'i', 'o', 'u'], true) ? 'an' : 'a',
|
||||
$type
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
174
src/testing/src/Fluent/AssertableJson.php
Normal file
174
src/testing/src/Fluent/AssertableJson.php
Normal file
@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
namespace Hyperf\Testing\Fluent;
|
||||
|
||||
use Closure;
|
||||
use Hyperf\Collection\Arr;
|
||||
use Hyperf\Contract\Arrayable;
|
||||
use Hyperf\Macroable\Macroable;
|
||||
use Hyperf\Tappable\Tappable;
|
||||
use Hyperf\Testing\AssertableJsonString;
|
||||
use PHPUnit\Framework\Assert as PHPUnit;
|
||||
|
||||
class AssertableJson implements Arrayable
|
||||
{
|
||||
use Concerns\Has;
|
||||
use Concerns\Matching;
|
||||
use Concerns\Debugging;
|
||||
use Concerns\Interaction;
|
||||
use Macroable;
|
||||
use Tappable;
|
||||
|
||||
/**
|
||||
* The properties in the current scope.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $props;
|
||||
|
||||
/**
|
||||
* The "dot" path to the current scope.
|
||||
*
|
||||
* @var null|string
|
||||
*/
|
||||
private $path;
|
||||
|
||||
/**
|
||||
* Create a new fluent, assertable JSON data instance.
|
||||
*/
|
||||
protected function __construct(array $props, string $path = null)
|
||||
{
|
||||
$this->path = $path;
|
||||
$this->props = $props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a new "scope" on the first child element.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function first(Closure $callback): self
|
||||
{
|
||||
$props = $this->prop();
|
||||
|
||||
$path = $this->dotPath();
|
||||
|
||||
PHPUnit::assertNotEmpty(
|
||||
$props,
|
||||
$path === ''
|
||||
? 'Cannot scope directly onto the first element of the root level because it is empty.'
|
||||
: sprintf('Cannot scope directly onto the first element of property [%s] because it is empty.', $path)
|
||||
);
|
||||
|
||||
$key = array_keys($props)[0];
|
||||
|
||||
$this->interactsWith($key);
|
||||
|
||||
return $this->scope($key, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a new "scope" on each child element.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function each(Closure $callback): self
|
||||
{
|
||||
$props = $this->prop();
|
||||
|
||||
$path = $this->dotPath();
|
||||
|
||||
PHPUnit::assertNotEmpty(
|
||||
$props,
|
||||
$path === ''
|
||||
? 'Cannot scope directly onto each element of the root level because it is empty.'
|
||||
: sprintf('Cannot scope directly onto each element of property [%s] because it is empty.', $path)
|
||||
);
|
||||
|
||||
foreach (array_keys($props) as $key) {
|
||||
$this->interactsWith($key);
|
||||
|
||||
$this->scope($key, $callback);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance from an array.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new static($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance from an AssertableJsonString.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function fromAssertableJsonString(AssertableJsonString $json): self
|
||||
{
|
||||
return static::fromArray($json->json());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the instance as an array.
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose the absolute "dot" path to the given key.
|
||||
*/
|
||||
protected function dotPath(string $key = ''): string
|
||||
{
|
||||
if (is_null($this->path)) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
return rtrim(implode('.', [$this->path, $key]), '.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a prop within the current scope using "dot" notation.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function prop(string $key = null)
|
||||
{
|
||||
return Arr::get($this->props, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a new "scope" at the path of the given key.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function scope(string $key, Closure $callback): self
|
||||
{
|
||||
$props = $this->prop($key);
|
||||
$path = $this->dotPath($key);
|
||||
|
||||
PHPUnit::assertIsArray($props, sprintf('Property [%s] is not scopeable.', $path));
|
||||
|
||||
$scope = new static($props, $path);
|
||||
$callback($scope);
|
||||
$scope->interacted();
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
44
src/testing/src/Fluent/Concerns/Debugging.php
Normal file
44
src/testing/src/Fluent/Concerns/Debugging.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
namespace Hyperf\Testing\Fluent\Concerns;
|
||||
|
||||
trait Debugging
|
||||
{
|
||||
/**
|
||||
* Dumps the given props.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function dump(string $prop = null): self
|
||||
{
|
||||
dump($this->prop($prop));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps the given props and exits.
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
public function dd(string $prop = null): void
|
||||
{
|
||||
dd($this->prop($prop));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a prop within the current scope using "dot" notation.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected function prop(string $key = null);
|
||||
}
|
209
src/testing/src/Fluent/Concerns/Has.php
Normal file
209
src/testing/src/Fluent/Concerns/Has.php
Normal file
@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
namespace Hyperf\Testing\Fluent\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Hyperf\Collection\Arr;
|
||||
use PHPUnit\Framework\Assert as PHPUnit;
|
||||
|
||||
trait Has
|
||||
{
|
||||
/**
|
||||
* Assert that the prop is of the expected size.
|
||||
*
|
||||
* @param int|string $key
|
||||
* @return $this
|
||||
*/
|
||||
public function count($key, int $length = null): self
|
||||
{
|
||||
if (is_null($length)) {
|
||||
$path = $this->dotPath();
|
||||
|
||||
PHPUnit::assertCount(
|
||||
$key,
|
||||
$this->prop(),
|
||||
$path
|
||||
? sprintf('Property [%s] does not have the expected size.', $path)
|
||||
: sprintf('Root level does not have the expected size.')
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
PHPUnit::assertCount(
|
||||
$length,
|
||||
$this->prop($key),
|
||||
sprintf('Property [%s] does not have the expected size.', $this->dotPath($key))
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the given prop exists.
|
||||
*
|
||||
* @param int|string $key
|
||||
* @param null|Closure|int $length
|
||||
* @return $this
|
||||
*/
|
||||
public function has($key, $length = null, Closure $callback = null): self
|
||||
{
|
||||
$prop = $this->prop();
|
||||
|
||||
if (is_int($key) && is_null($length)) {
|
||||
return $this->count($key);
|
||||
}
|
||||
|
||||
PHPUnit::assertTrue(
|
||||
Arr::has($prop, $key),
|
||||
sprintf('Property [%s] does not exist.', $this->dotPath($key))
|
||||
);
|
||||
|
||||
$this->interactsWith($key);
|
||||
|
||||
if (! is_null($callback)) {
|
||||
return $this->has($key, function (self $scope) use ($length, $callback) {
|
||||
return $scope
|
||||
->tap(function (self $scope) use ($length) {
|
||||
if (! is_null($length)) {
|
||||
$scope->count($length);
|
||||
}
|
||||
})
|
||||
->first($callback)
|
||||
->etc();
|
||||
});
|
||||
}
|
||||
|
||||
if (is_callable($length)) {
|
||||
return $this->scope($key, $length);
|
||||
}
|
||||
|
||||
if (! is_null($length)) {
|
||||
return $this->count($key, $length);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that all of the given props exist.
|
||||
*
|
||||
* @param array|string $key
|
||||
* @return $this
|
||||
*/
|
||||
public function hasAll($key): self
|
||||
{
|
||||
$keys = is_array($key) ? $key : func_get_args();
|
||||
|
||||
foreach ($keys as $prop => $count) {
|
||||
if (is_int($prop)) {
|
||||
$this->has($count);
|
||||
} else {
|
||||
$this->has($prop, $count);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that at least one of the given props exists.
|
||||
*
|
||||
* @param array|string $key
|
||||
* @return $this
|
||||
*/
|
||||
public function hasAny($key): self
|
||||
{
|
||||
$keys = is_array($key) ? $key : func_get_args();
|
||||
|
||||
PHPUnit::assertTrue(
|
||||
Arr::hasAny($this->prop(), $keys),
|
||||
sprintf('None of properties [%s] exist.', implode(', ', $keys))
|
||||
);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$this->interactsWith($key);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that none of the given props exist.
|
||||
*
|
||||
* @param array|string $key
|
||||
* @return $this
|
||||
*/
|
||||
public function missingAll($key): self
|
||||
{
|
||||
$keys = is_array($key) ? $key : func_get_args();
|
||||
|
||||
foreach ($keys as $prop) {
|
||||
$this->missing($prop);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given prop does not exist.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function missing(string $key): self
|
||||
{
|
||||
PHPUnit::assertNotTrue(
|
||||
Arr::has($this->prop(), $key),
|
||||
sprintf('Property [%s] was found while it was expected to be missing.', $this->dotPath($key))
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the interaction check.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
abstract public function etc();
|
||||
|
||||
/**
|
||||
* Instantiate a new "scope" on the first element.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
abstract public function first(Closure $callback);
|
||||
|
||||
/**
|
||||
* Compose the absolute "dot" path to the given key.
|
||||
*/
|
||||
abstract protected function dotPath(string $key = ''): string;
|
||||
|
||||
/**
|
||||
* Marks the property as interacted.
|
||||
*/
|
||||
abstract protected function interactsWith(string $key): void;
|
||||
|
||||
/**
|
||||
* Retrieve a prop within the current scope using "dot" notation.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected function prop(string $key = null);
|
||||
|
||||
/**
|
||||
* Instantiate a new "scope" at the path of the given key.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
abstract protected function scope(string $key, Closure $callback);
|
||||
}
|
70
src/testing/src/Fluent/Concerns/Interaction.php
Normal file
70
src/testing/src/Fluent/Concerns/Interaction.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
namespace Hyperf\Testing\Fluent\Concerns;
|
||||
|
||||
use Hyperf\Stringable\Str;
|
||||
use PHPUnit\Framework\Assert as PHPUnit;
|
||||
|
||||
trait Interaction
|
||||
{
|
||||
/**
|
||||
* The list of interacted properties.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $interacted = [];
|
||||
|
||||
/**
|
||||
* Asserts that all properties have been interacted with.
|
||||
*/
|
||||
public function interacted(): void
|
||||
{
|
||||
PHPUnit::assertSame(
|
||||
[],
|
||||
array_diff(array_keys($this->prop()), $this->interacted),
|
||||
$this->path
|
||||
? sprintf('Unexpected properties were found in scope [%s].', $this->path)
|
||||
: 'Unexpected properties were found on the root level.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the interaction check.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function etc(): self
|
||||
{
|
||||
$this->interacted = array_keys($this->prop());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the property as interacted.
|
||||
*/
|
||||
protected function interactsWith(string $key): void
|
||||
{
|
||||
$prop = Str::before($key, '.');
|
||||
|
||||
if (! in_array($prop, $this->interacted, true)) {
|
||||
$this->interacted[] = $prop;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a prop within the current scope using "dot" notation.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected function prop(string $key = null);
|
||||
}
|
232
src/testing/src/Fluent/Concerns/Matching.php
Normal file
232
src/testing/src/Fluent/Concerns/Matching.php
Normal file
@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
namespace Hyperf\Testing\Fluent\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Hyperf\Collection\Collection;
|
||||
use Hyperf\Contract\Arrayable;
|
||||
use PHPUnit\Framework\Assert as PHPUnit;
|
||||
|
||||
trait Matching
|
||||
{
|
||||
/**
|
||||
* Asserts that the property matches the expected value.
|
||||
*
|
||||
* @param Closure|mixed $expected
|
||||
* @return $this
|
||||
*/
|
||||
public function where(string $key, $expected): self
|
||||
{
|
||||
$this->has($key);
|
||||
|
||||
$actual = $this->prop($key);
|
||||
|
||||
if ($expected instanceof Closure) {
|
||||
PHPUnit::assertTrue(
|
||||
$expected(is_array($actual) ? Collection::make($actual) : $actual),
|
||||
sprintf('Property [%s] was marked as invalid using a closure.', $this->dotPath($key))
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($expected instanceof Arrayable) {
|
||||
$expected = $expected->toArray();
|
||||
}
|
||||
|
||||
$this->ensureSorted($expected);
|
||||
$this->ensureSorted($actual);
|
||||
|
||||
PHPUnit::assertSame(
|
||||
$expected,
|
||||
$actual,
|
||||
sprintf('Property [%s] does not match the expected value.', $this->dotPath($key))
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the property does not match the expected value.
|
||||
*
|
||||
* @param Closure|mixed $expected
|
||||
* @return $this
|
||||
*/
|
||||
public function whereNot(string $key, $expected): self
|
||||
{
|
||||
$this->has($key);
|
||||
|
||||
$actual = $this->prop($key);
|
||||
|
||||
if ($expected instanceof Closure) {
|
||||
PHPUnit::assertFalse(
|
||||
$expected(is_array($actual) ? Collection::make($actual) : $actual),
|
||||
sprintf('Property [%s] was marked as invalid using a closure.', $this->dotPath($key))
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($expected instanceof Arrayable) {
|
||||
$expected = $expected->toArray();
|
||||
}
|
||||
|
||||
$this->ensureSorted($expected);
|
||||
$this->ensureSorted($actual);
|
||||
|
||||
PHPUnit::assertNotSame(
|
||||
$expected,
|
||||
$actual,
|
||||
sprintf(
|
||||
'Property [%s] contains a value that should be missing: [%s, %s]',
|
||||
$this->dotPath($key),
|
||||
$key,
|
||||
$expected
|
||||
)
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that all properties match their expected values.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function whereAll(array $bindings): self
|
||||
{
|
||||
foreach ($bindings as $key => $value) {
|
||||
$this->where($key, $value);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the property is of the expected type.
|
||||
*
|
||||
* @param array|string $expected
|
||||
* @return $this
|
||||
*/
|
||||
public function whereType(string $key, $expected): self
|
||||
{
|
||||
$this->has($key);
|
||||
|
||||
$actual = $this->prop($key);
|
||||
|
||||
if (! is_array($expected)) {
|
||||
$expected = explode('|', $expected);
|
||||
}
|
||||
|
||||
PHPUnit::assertContains(
|
||||
strtolower(gettype($actual)),
|
||||
$expected,
|
||||
sprintf('Property [%s] is not of expected type [%s].', $this->dotPath($key), implode('|', $expected))
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that all properties are of their expected types.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function whereAllType(array $bindings): self
|
||||
{
|
||||
foreach ($bindings as $key => $value) {
|
||||
$this->whereType($key, $value);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the property contains the expected values.
|
||||
*
|
||||
* @param mixed $expected
|
||||
* @return $this
|
||||
*/
|
||||
public function whereContains(string $key, $expected)
|
||||
{
|
||||
$actual = Collection::make(
|
||||
$this->prop($key) ?? $this->prop()
|
||||
);
|
||||
|
||||
$missing = Collection::make($expected)->reject(function ($search) use ($key, $actual) {
|
||||
if ($actual->containsStrict($key, $search)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $actual->containsStrict($search);
|
||||
});
|
||||
|
||||
if ($missing->whereInstanceOf('Closure')->isNotEmpty()) {
|
||||
PHPUnit::assertEmpty(
|
||||
$missing->toArray(),
|
||||
sprintf(
|
||||
'Property [%s] does not contain a value that passes the truth test within the given closure.',
|
||||
$key,
|
||||
)
|
||||
);
|
||||
} else {
|
||||
PHPUnit::assertEmpty(
|
||||
$missing->toArray(),
|
||||
sprintf(
|
||||
'Property [%s] does not contain [%s].',
|
||||
$key,
|
||||
implode(', ', array_values($missing->toArray()))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the given prop exists.
|
||||
*
|
||||
* @param null $value
|
||||
* @return $this
|
||||
*/
|
||||
abstract public function has(string $key, $value = null, Closure $scope = null);
|
||||
|
||||
/**
|
||||
* Ensures that all properties are sorted the same way, recursively.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
protected function ensureSorted(&$value): void
|
||||
{
|
||||
if (! is_array($value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($value as &$arg) {
|
||||
$this->ensureSorted($arg);
|
||||
}
|
||||
|
||||
ksort($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose the absolute "dot" path to the given key.
|
||||
*/
|
||||
abstract protected function dotPath(string $key = ''): string;
|
||||
|
||||
/**
|
||||
* Retrieve a prop within the current scope using "dot" notation.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected function prop(string $key = null);
|
||||
}
|
270
src/testing/src/Http/Client.php
Normal file
270
src/testing/src/Http/Client.php
Normal file
@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
namespace Hyperf\Testing\Http;
|
||||
|
||||
use Hyperf\Collection\Arr;
|
||||
use Hyperf\Context\Context;
|
||||
use Hyperf\Contract\ConfigInterface;
|
||||
use Hyperf\Contract\PackerInterface;
|
||||
use Hyperf\Dispatcher\HttpDispatcher;
|
||||
use Hyperf\ExceptionHandler\ExceptionHandlerDispatcher;
|
||||
use Hyperf\HttpMessage\Server\Request as Psr7Request;
|
||||
use Hyperf\HttpMessage\Server\Response as Psr7Response;
|
||||
use Hyperf\HttpMessage\Stream\SwooleStream;
|
||||
use Hyperf\HttpMessage\Uri\Uri;
|
||||
use Hyperf\HttpServer\MiddlewareManager;
|
||||
use Hyperf\HttpServer\ResponseEmitter;
|
||||
use Hyperf\HttpServer\Router\Dispatched;
|
||||
use Hyperf\HttpServer\Server;
|
||||
use Hyperf\Support\Filesystem\Filesystem;
|
||||
use Hyperf\Testing\HttpMessage\Upload\UploadedFile;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Throwable;
|
||||
|
||||
use function Hyperf\Collection\data_get;
|
||||
use function Hyperf\Coroutine\wait;
|
||||
|
||||
class Client extends Server
|
||||
{
|
||||
protected PackerInterface $packer;
|
||||
|
||||
protected float $waitTimeout = 10.0;
|
||||
|
||||
protected string $baseUri = 'http://127.0.0.1/';
|
||||
|
||||
public function __construct(ContainerInterface $container, $server = 'http')
|
||||
{
|
||||
parent::__construct(
|
||||
$container,
|
||||
$container->get(HttpDispatcher::class),
|
||||
$container->get(ExceptionHandlerDispatcher::class),
|
||||
$container->get(ResponseEmitter::class)
|
||||
);
|
||||
|
||||
$this->initCoreMiddleware($server);
|
||||
$this->initBaseUri($server);
|
||||
}
|
||||
|
||||
public function get(string $uri, array $data = [], array $headers = [])
|
||||
{
|
||||
return $this->request('GET', $uri, [
|
||||
'headers' => $headers,
|
||||
'query' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
public function post(string $uri, array $data = [], array $headers = [])
|
||||
{
|
||||
return $this->request('POST', $uri, [
|
||||
'headers' => $headers,
|
||||
'form_params' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
public function put(string $uri, array $data = [], array $headers = [])
|
||||
{
|
||||
return $this->request('PUT', $uri, [
|
||||
'headers' => $headers,
|
||||
'form_params' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete(string $uri, array $data = [], array $headers = [])
|
||||
{
|
||||
return $this->request('DELETE', $uri, [
|
||||
'headers' => $headers,
|
||||
'query' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
public function json(string $uri, array $data = [], array $headers = [])
|
||||
{
|
||||
$headers['Content-Type'] = 'application/json';
|
||||
|
||||
return $this->request('POST', $uri, [
|
||||
'headers' => $headers,
|
||||
'json' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
public function file(string $uri, array $data = [], array $headers = [])
|
||||
{
|
||||
$multipart = [];
|
||||
|
||||
if (Arr::isAssoc($data)) {
|
||||
$data = [$data];
|
||||
}
|
||||
|
||||
foreach ($data as $item) {
|
||||
$name = $item['name'];
|
||||
$file = $item['file'];
|
||||
|
||||
$multipart[] = [
|
||||
'name' => $name,
|
||||
'contents' => fopen($file, 'r'),
|
||||
'filename' => basename($file),
|
||||
];
|
||||
}
|
||||
|
||||
return $this->request('POST', $uri, [
|
||||
'headers' => $headers,
|
||||
'multipart' => $multipart,
|
||||
]);
|
||||
}
|
||||
|
||||
public function request(string $method, string $path, array $options = [])
|
||||
{
|
||||
return wait(function () use ($method, $path, $options) {
|
||||
return $this->execute($this->initRequest($method, $path, $options));
|
||||
}, $this->waitTimeout);
|
||||
}
|
||||
|
||||
public function sendRequest(ServerRequestInterface $psr7Request): ResponseInterface
|
||||
{
|
||||
return wait(function () use ($psr7Request) {
|
||||
return $this->execute($psr7Request);
|
||||
}, $this->waitTimeout);
|
||||
}
|
||||
|
||||
public function initRequest(string $method, string $path, array $options = []): ServerRequestInterface
|
||||
{
|
||||
$query = $options['query'] ?? [];
|
||||
$params = $options['form_params'] ?? [];
|
||||
$json = $options['json'] ?? [];
|
||||
$headers = $options['headers'] ?? [];
|
||||
$multipart = $options['multipart'] ?? [];
|
||||
|
||||
$parsePath = parse_url($path);
|
||||
$path = $parsePath['path'];
|
||||
$uriPathQuery = $parsePath['query'] ?? [];
|
||||
|
||||
if (! empty($uriPathQuery)) {
|
||||
parse_str($uriPathQuery, $pathQuery);
|
||||
$query = array_merge($pathQuery, $query);
|
||||
}
|
||||
|
||||
$data = $params;
|
||||
|
||||
// Initialize PSR-7 Request and Response objects.
|
||||
$uri = (new Uri($this->baseUri . ltrim($path, '/')))->withQuery(http_build_query($query));
|
||||
|
||||
$content = http_build_query($params);
|
||||
if ($method == 'POST' && data_get($headers, 'Content-Type') == 'application/json') {
|
||||
$content = json_encode($json, JSON_UNESCAPED_UNICODE);
|
||||
$data = $json;
|
||||
}
|
||||
|
||||
$body = new SwooleStream($content);
|
||||
|
||||
$request = new Psr7Request($method, $uri, $headers, $body);
|
||||
|
||||
return $request->withQueryParams($query)
|
||||
->withParsedBody($data)
|
||||
->withUploadedFiles($this->normalizeFiles($multipart));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated It will be removed in v3.0
|
||||
*/
|
||||
protected function init(string $method, string $path, array $options = []): ServerRequestInterface
|
||||
{
|
||||
return $this->initRequest($method, $path, $options);
|
||||
}
|
||||
|
||||
protected function execute(ServerRequestInterface $psr7Request): ResponseInterface
|
||||
{
|
||||
$this->persistToContext($psr7Request, new Psr7Response());
|
||||
|
||||
$psr7Request = $this->coreMiddleware->dispatch($psr7Request);
|
||||
/** @var Dispatched $dispatched */
|
||||
$dispatched = $psr7Request->getAttribute(Dispatched::class);
|
||||
$middlewares = $this->middlewares;
|
||||
if ($dispatched->isFound()) {
|
||||
$registeredMiddlewares = MiddlewareManager::get($this->serverName, $dispatched->handler->route, $psr7Request->getMethod());
|
||||
$middlewares = array_merge($middlewares, $registeredMiddlewares);
|
||||
}
|
||||
|
||||
try {
|
||||
$psr7Response = $this->dispatcher->dispatch($psr7Request, $middlewares, $this->coreMiddleware);
|
||||
} catch (Throwable $throwable) {
|
||||
// Delegate the exception to exception handler.
|
||||
$psr7Response = $this->exceptionHandlerDispatcher->dispatch($throwable, $this->exceptionHandlers);
|
||||
}
|
||||
|
||||
return $psr7Response;
|
||||
}
|
||||
|
||||
protected function persistToContext(ServerRequestInterface $request, ResponseInterface $response)
|
||||
{
|
||||
Context::set(ServerRequestInterface::class, $request);
|
||||
Context::set(ResponseInterface::class, $response);
|
||||
}
|
||||
|
||||
protected function initBaseUri(string $server): void
|
||||
{
|
||||
if ($this->container->has(ConfigInterface::class)) {
|
||||
$config = $this->container->get(ConfigInterface::class);
|
||||
$servers = $config->get('server.servers', []);
|
||||
foreach ($servers as $item) {
|
||||
if ($item['name'] == $server) {
|
||||
$this->baseUri = sprintf('http://127.0.0.1:%d/', (int) $item['port']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function normalizeFiles(array $multipart): array
|
||||
{
|
||||
$files = [];
|
||||
$fileSystem = $this->container->get(Filesystem::class);
|
||||
|
||||
foreach ($multipart as $item) {
|
||||
if (isset($item['name'], $item['contents'], $item['filename'])) {
|
||||
$name = $item['name'];
|
||||
$contents = $item['contents'];
|
||||
$filename = $item['filename'];
|
||||
|
||||
$dir = BASE_PATH . '/runtime/uploads';
|
||||
$tmpName = $dir . '/' . $filename;
|
||||
if (! is_dir($dir)) {
|
||||
$fileSystem->makeDirectory($dir);
|
||||
}
|
||||
$fileSystem->put($tmpName, $contents);
|
||||
|
||||
$stats = fstat($contents);
|
||||
|
||||
$files[$name] = new UploadedFile(
|
||||
$tmpName,
|
||||
$stats['size'],
|
||||
0,
|
||||
$name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
protected function getStream(string $resource)
|
||||
{
|
||||
$stream = fopen('php://temp', 'r+');
|
||||
if ($resource !== '') {
|
||||
fwrite($stream, $resource);
|
||||
fseek($stream, 0);
|
||||
}
|
||||
|
||||
return $stream;
|
||||
}
|
||||
}
|
172
src/testing/src/Http/Concerns/AssertsStatusCodes.php
Normal file
172
src/testing/src/Http/Concerns/AssertsStatusCodes.php
Normal file
@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
namespace Hyperf\Testing\Http\Concerns;
|
||||
|
||||
use PHPUnit\Framework\Assert as PHPUnit;
|
||||
|
||||
trait AssertsStatusCodes
|
||||
{
|
||||
/**
|
||||
* Assert that the response has a 200 "OK" status code.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertOk(): self
|
||||
{
|
||||
return $this->assertStatus(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has a 201 "Created" status code.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertCreated(): self
|
||||
{
|
||||
return $this->assertStatus(201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has a 202 "Accepted" status code.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertAccepted(): self
|
||||
{
|
||||
return $this->assertStatus(202);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has the given status code and no content.
|
||||
*
|
||||
* @param int $status
|
||||
* @return $this
|
||||
*/
|
||||
public function assertNoContent($status = 204): self
|
||||
{
|
||||
$this->assertStatus($status);
|
||||
|
||||
PHPUnit::assertEmpty($this->getContent(), 'Response content is not empty.');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has a 301 "Moved Permanently" status code.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertMovedPermanently(): self
|
||||
{
|
||||
return $this->assertStatus(301);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has a 302 "Found" status code.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertFound(): self
|
||||
{
|
||||
return $this->assertStatus(302);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has a 400 "Bad Request" status code.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertBadRequest(): self
|
||||
{
|
||||
return $this->assertStatus(400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has a 401 "Unauthorized" status code.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertUnauthorized(): self
|
||||
{
|
||||
return $this->assertStatus(401);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has a 402 "Payment Required" status code.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertPaymentRequired(): self
|
||||
{
|
||||
return $this->assertStatus(402);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has a 403 "Forbidden" status code.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertForbidden(): self
|
||||
{
|
||||
return $this->assertStatus(403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has a 404 "Not Found" status code.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertNotFound(): self
|
||||
{
|
||||
return $this->assertStatus(404);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has a 408 "Request Timeout" status code.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertRequestTimeout(): self
|
||||
{
|
||||
return $this->assertStatus(408);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has a 409 "Conflict" status code.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertConflict(): self
|
||||
{
|
||||
return $this->assertStatus(409);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has a 422 "Unprocessable Entity" status code.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertUnprocessable(): self
|
||||
{
|
||||
return $this->assertStatus(422);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has a 429 "Too Many Requests" status code.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertTooManyRequests(): self
|
||||
{
|
||||
return $this->assertStatus(429);
|
||||
}
|
||||
}
|
721
src/testing/src/Http/TestResponse.php
Normal file
721
src/testing/src/Http/TestResponse.php
Normal file
@ -0,0 +1,721 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
namespace Hyperf\Testing\Http;
|
||||
|
||||
use ArrayAccess;
|
||||
use Hyperf\Collection\Arr;
|
||||
use Hyperf\Collection\Collection;
|
||||
use Hyperf\HttpServer\Response;
|
||||
use Hyperf\Macroable\Macroable;
|
||||
use Hyperf\Stringable\Str;
|
||||
use Hyperf\Tappable\Tappable;
|
||||
use Hyperf\Testing\AssertableJsonString;
|
||||
use Hyperf\Testing\Constraint\SeeInOrder;
|
||||
use Hyperf\Testing\Fluent\AssertableJson;
|
||||
use LogicException;
|
||||
use PHPUnit\Framework\Assert as PHPUnit;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @mixin Response
|
||||
*/
|
||||
class TestResponse implements ArrayAccess
|
||||
{
|
||||
use Concerns\AssertsStatusCodes, Tappable, Macroable {
|
||||
__call as macroCall;
|
||||
}
|
||||
|
||||
protected ?array $decoded = null;
|
||||
|
||||
/**
|
||||
* The streamed content of the response.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $streamedContent;
|
||||
|
||||
public function __construct(protected ResponseInterface $response)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle dynamic calls into macros or pass missing methods to the base response.
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $args
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call($method, $args)
|
||||
{
|
||||
if (static::hasMacro($method)) {
|
||||
return $this->macroCall($method, $args);
|
||||
}
|
||||
|
||||
return $this->response->{$method}(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically access base response parameters.
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get($key)
|
||||
{
|
||||
return $this->response->{$key};
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy isset() checks to the underlying base response.
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function __isset($key)
|
||||
{
|
||||
return isset($this->response->{$key});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content of the response.
|
||||
*/
|
||||
public function getContent(): string
|
||||
{
|
||||
return $this->response->getBody()->getContents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given string matches the response content.
|
||||
*
|
||||
* @param string $value
|
||||
* @return $this
|
||||
*/
|
||||
public function assertContent($value)
|
||||
{
|
||||
PHPUnit::assertSame($value, $this->getContent());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given string matches the streamed response content.
|
||||
*
|
||||
* @param string $value
|
||||
* @return $this
|
||||
*/
|
||||
public function assertStreamedContent($value)
|
||||
{
|
||||
PHPUnit::assertSame($value, $this->streamedContent());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given string or array of strings are contained within the response.
|
||||
*
|
||||
* @param array|string $value
|
||||
* @param bool $escape
|
||||
* @return $this
|
||||
*/
|
||||
public function assertSee($value, $escape = true)
|
||||
{
|
||||
$value = Arr::wrap($value);
|
||||
|
||||
$values = $escape ? array_map(fn ($value) => htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', true), $value) : $value;
|
||||
|
||||
foreach ($values as $value) {
|
||||
PHPUnit::assertStringContainsString((string) $value, $this->getContent());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given strings are contained in order within the response.
|
||||
*
|
||||
* @param bool $escape
|
||||
* @return $this
|
||||
*/
|
||||
public function assertSeeInOrder(array $values, $escape = true)
|
||||
{
|
||||
$values = $escape ? array_map(fn ($value) => htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', true), $values) : $values;
|
||||
|
||||
PHPUnit::assertThat($values, new SeeInOrder($this->getContent()));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given string or array of strings are contained within the response text.
|
||||
*
|
||||
* @param array|string $value
|
||||
* @param bool $escape
|
||||
* @return $this
|
||||
*/
|
||||
public function assertSeeText($value, $escape = true)
|
||||
{
|
||||
$value = Arr::wrap($value);
|
||||
|
||||
$values = $escape ? array_map(fn ($value) => htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', true), $value) : $value;
|
||||
|
||||
$content = strip_tags($this->getContent());
|
||||
|
||||
foreach ($values as $value) {
|
||||
PHPUnit::assertStringContainsString((string) $value, $content);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given strings are contained in order within the response text.
|
||||
*
|
||||
* @param bool $escape
|
||||
* @return $this
|
||||
*/
|
||||
public function assertSeeTextInOrder(array $values, $escape = true)
|
||||
{
|
||||
$values = $escape ? array_map(fn ($value) => htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', true), $values) : $values;
|
||||
|
||||
PHPUnit::assertThat($values, new SeeInOrder(strip_tags($this->getContent())));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given string or array of strings are not contained within the response.
|
||||
*
|
||||
* @param array|string $value
|
||||
* @param bool $escape
|
||||
* @return $this
|
||||
*/
|
||||
public function assertDontSee($value, $escape = true)
|
||||
{
|
||||
$value = Arr::wrap($value);
|
||||
|
||||
$values = $escape ? array_map(fn ($value) => htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', true), $value) : $value;
|
||||
|
||||
foreach ($values as $value) {
|
||||
PHPUnit::assertStringNotContainsString((string) $value, $this->getContent());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given string or array of strings are not contained within the response text.
|
||||
*
|
||||
* @param array|string $value
|
||||
* @param bool $escape
|
||||
* @return $this
|
||||
*/
|
||||
public function assertDontSeeText($value, $escape = true)
|
||||
{
|
||||
$value = Arr::wrap($value);
|
||||
|
||||
$values = $escape ? array_map(fn ($value) => htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', true), $value) : $value;
|
||||
|
||||
$content = strip_tags($this->getContent());
|
||||
|
||||
foreach ($values as $value) {
|
||||
PHPUnit::assertStringNotContainsString((string) $value, $content);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response is a superset of the given JSON.
|
||||
*
|
||||
* @param array|callable $value
|
||||
* @param bool $strict
|
||||
* @return $this
|
||||
*/
|
||||
public function assertJson($value, $strict = false)
|
||||
{
|
||||
$json = $this->decodeResponseJson();
|
||||
|
||||
if (is_array($value)) {
|
||||
$json->assertSubset($value, $strict);
|
||||
} else {
|
||||
$assert = AssertableJson::fromAssertableJsonString($json);
|
||||
|
||||
$value($assert);
|
||||
|
||||
if (Arr::isAssoc($assert->toArray())) {
|
||||
$assert->interacted();
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the expected value and type exists at the given path in the response.
|
||||
*
|
||||
* @param string $path
|
||||
* @param mixed $expect
|
||||
* @return $this
|
||||
*/
|
||||
public function assertJsonPath($path, $expect)
|
||||
{
|
||||
$this->decodeResponseJson()->assertPath($path, $expect);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has the exact given JSON.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertExactJson(array $data)
|
||||
{
|
||||
$this->decodeResponseJson()->assertExact($data);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has the similar JSON as given.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertSimilarJson(array $data)
|
||||
{
|
||||
$this->decodeResponseJson()->assertSimilar($data);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response contains the given JSON fragment.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertJsonFragment(array $data)
|
||||
{
|
||||
$this->decodeResponseJson()->assertFragment($data);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response does not contain the given JSON fragment.
|
||||
*
|
||||
* @param bool $exact
|
||||
* @return $this
|
||||
*/
|
||||
public function assertJsonMissing(array $data, $exact = false)
|
||||
{
|
||||
$this->decodeResponseJson()->assertMissing($data, $exact);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response does not contain the exact JSON fragment.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertJsonMissingExact(array $data)
|
||||
{
|
||||
$this->decodeResponseJson()->assertMissingExact($data);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response does not contain the given path.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function assertJsonMissingPath(string $path)
|
||||
{
|
||||
$this->decodeResponseJson()->assertMissingPath($path);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has a given JSON structure.
|
||||
*
|
||||
* @param null|array $responseData
|
||||
* @return $this
|
||||
*/
|
||||
public function assertJsonStructure(array $structure = null, $responseData = null)
|
||||
{
|
||||
$this->decodeResponseJson()->assertStructure($structure, $responseData);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response JSON has the expected count of items at the given key.
|
||||
*
|
||||
* @param null|string $key
|
||||
* @return $this
|
||||
*/
|
||||
public function assertJsonCount(int $count, $key = null)
|
||||
{
|
||||
$this->decodeResponseJson()->assertCount($count, $key);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has the given JSON validation errors.
|
||||
*
|
||||
* @param array|string $errors
|
||||
* @param string $responseKey
|
||||
* @return $this
|
||||
*/
|
||||
public function assertJsonValidationErrors($errors, $responseKey = 'errors')
|
||||
{
|
||||
$errors = Arr::wrap($errors);
|
||||
|
||||
PHPUnit::assertNotEmpty($errors, 'No validation errors were provided.');
|
||||
|
||||
$jsonErrors = Arr::get($this->json(), $responseKey) ?? [];
|
||||
|
||||
$errorMessage = $jsonErrors
|
||||
? 'Response has the following JSON validation errors:' .
|
||||
PHP_EOL . PHP_EOL . json_encode($jsonErrors, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL
|
||||
: 'Response does not have JSON validation errors.';
|
||||
|
||||
foreach ($errors as $key => $value) {
|
||||
if (is_int($key)) {
|
||||
$this->assertJsonValidationErrorFor($value, $responseKey);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->assertJsonValidationErrorFor($key, $responseKey);
|
||||
|
||||
foreach (Arr::wrap($value) as $expectedMessage) {
|
||||
$errorMissing = true;
|
||||
|
||||
foreach (Arr::wrap($jsonErrors[$key]) as $jsonErrorMessage) {
|
||||
if (Str::contains($jsonErrorMessage, $expectedMessage)) {
|
||||
$errorMissing = false;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($errorMissing) { /* @phpstan-ignore-line */
|
||||
PHPUnit::fail(
|
||||
"Failed to find a validation error in the response for key and message: '{$key}' => '{$expectedMessage}'" . PHP_EOL . PHP_EOL . $errorMessage /* @phpstan-ignore-line */
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert the response has any JSON validation errors for the given key.
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $responseKey
|
||||
* @return $this
|
||||
*/
|
||||
public function assertJsonValidationErrorFor($key, $responseKey = 'errors')
|
||||
{
|
||||
$jsonErrors = Arr::get($this->json(), $responseKey) ?? [];
|
||||
|
||||
$errorMessage = $jsonErrors
|
||||
? 'Response has the following JSON validation errors:' .
|
||||
PHP_EOL . PHP_EOL . json_encode($jsonErrors, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL
|
||||
: 'Response does not have JSON validation errors.';
|
||||
|
||||
PHPUnit::assertArrayHasKey(
|
||||
$key,
|
||||
$jsonErrors,
|
||||
"Failed to find a validation error in the response for key: '{$key}'" . PHP_EOL . PHP_EOL . $errorMessage
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has no JSON validation errors for the given keys.
|
||||
*
|
||||
* @param null|array|string $keys
|
||||
* @param string $responseKey
|
||||
* @return $this
|
||||
*/
|
||||
public function assertJsonMissingValidationErrors($keys = null, $responseKey = 'errors')
|
||||
{
|
||||
if ($this->getContent() === '') {
|
||||
PHPUnit::assertTrue(true);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$json = $this->json();
|
||||
|
||||
if (! Arr::has($json, $responseKey)) {
|
||||
PHPUnit::assertTrue(true);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$errors = Arr::get($json, $responseKey, []);
|
||||
|
||||
if (is_null($keys) && count($errors) > 0) {
|
||||
PHPUnit::fail(
|
||||
'Response has unexpected validation errors: ' . PHP_EOL . PHP_EOL .
|
||||
json_encode($errors, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
}
|
||||
|
||||
foreach (Arr::wrap($keys) as $key) {
|
||||
PHPUnit::assertFalse(
|
||||
isset($errors[$key]),
|
||||
"Found unexpected validation error for key: '{$key}'"
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given key is a JSON array.
|
||||
*
|
||||
* @param null|mixed $key
|
||||
* @return $this
|
||||
*/
|
||||
public function assertJsonIsArray($key = null)
|
||||
{
|
||||
$data = $this->json($key);
|
||||
|
||||
$encodedData = json_encode($data);
|
||||
|
||||
PHPUnit::assertTrue(
|
||||
is_array($data)
|
||||
&& str_starts_with($encodedData, '[')
|
||||
&& str_ends_with($encodedData, ']')
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given key is a JSON object.
|
||||
*
|
||||
* @param null|mixed $key
|
||||
* @return $this
|
||||
*/
|
||||
public function assertJsonIsObject($key = null)
|
||||
{
|
||||
$data = $this->json($key);
|
||||
|
||||
$encodedData = json_encode($data);
|
||||
|
||||
PHPUnit::assertTrue(
|
||||
is_array($data)
|
||||
&& str_starts_with($encodedData, '{')
|
||||
&& str_ends_with($encodedData, '}')
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and return the decoded response JSON.
|
||||
*
|
||||
* @return AssertableJsonString
|
||||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function decodeResponseJson()
|
||||
{
|
||||
$testJson = new AssertableJsonString($this->getContent());
|
||||
|
||||
$decodedResponse = $testJson->json();
|
||||
|
||||
if (is_null($decodedResponse) || $decodedResponse === false) {
|
||||
if ($this->exception ?? null) {
|
||||
throw $this->exception;
|
||||
}
|
||||
PHPUnit::fail('Invalid JSON was returned from the route.');
|
||||
}
|
||||
|
||||
return $testJson;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON decoded body of the response as an array or scalar value.
|
||||
*
|
||||
* @param null|string $key
|
||||
*/
|
||||
public function json($key = null): mixed
|
||||
{
|
||||
return $this->decodeResponseJson()->json($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the JSON decoded body of the response as a collection.
|
||||
*
|
||||
* @param null|string $key
|
||||
*/
|
||||
public function collect($key = null): Collection
|
||||
{
|
||||
return Collection::make($this->json($key));
|
||||
}
|
||||
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
return isset($this->json()[$offset]);
|
||||
}
|
||||
|
||||
public function offsetGet(mixed $offset): mixed
|
||||
{
|
||||
return $this->json()[$offset];
|
||||
}
|
||||
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
throw new LogicException('Response data may not be mutated using array access.');
|
||||
}
|
||||
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
throw new LogicException('Response data may not be mutated using array access.');
|
||||
}
|
||||
|
||||
public static function fromBaseResponse(ResponseInterface $response)
|
||||
{
|
||||
return new static(new Response($response));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has a successful status code.
|
||||
*/
|
||||
public function assertSuccessful(): self
|
||||
{
|
||||
PHPUnit::assertTrue(
|
||||
$this->isSuccessful(),
|
||||
$this->statusMessageWithDetails('>=200, <300', $this->getStatusCode())
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response has the given status code.
|
||||
*
|
||||
* @param int $status
|
||||
* @return $this
|
||||
*/
|
||||
public function assertStatus($status): self
|
||||
{
|
||||
$message = $this->statusMessageWithDetails($status, $actual = $this->getStatusCode());
|
||||
|
||||
PHPUnit::assertSame($actual, $status, $message);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is response successful?
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
public function isSuccessful(): bool
|
||||
{
|
||||
return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300;
|
||||
}
|
||||
|
||||
/**
|
||||
* Was there a server side error?
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
public function isServerError(): bool
|
||||
{
|
||||
return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the response is a server error.
|
||||
*/
|
||||
public function assertServerError(): self
|
||||
{
|
||||
PHPUnit::assertTrue(
|
||||
$this->isServerError(),
|
||||
$this->statusMessageWithDetails('>=500, < 600', $this->getStatusCode())
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
return $this->response->getStatusCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the streamed content from the response.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function streamedContent()
|
||||
{
|
||||
if (! is_null($this->streamedContent)) {
|
||||
return $this->streamedContent;
|
||||
}
|
||||
|
||||
if (! $this->response instanceof StreamedResponse) {
|
||||
PHPUnit::fail('The response is not a streamed response.');
|
||||
}
|
||||
|
||||
ob_start(function (string $buffer): string {
|
||||
$this->streamedContent .= $buffer;
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
$this->sendContent();
|
||||
|
||||
ob_end_clean();
|
||||
|
||||
return $this->streamedContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends content for the current web response.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function sendContent(): static
|
||||
{
|
||||
echo $this->streamedContent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an assertion message for a status assertion containing extra details when available.
|
||||
*
|
||||
* @param int|string $expected
|
||||
* @param int|string $actual
|
||||
*/
|
||||
protected function statusMessageWithDetails($expected, $actual): string
|
||||
{
|
||||
return "Expected response status code [{$expected}] but received {$actual}.";
|
||||
}
|
||||
}
|
@ -20,6 +20,9 @@ use Hyperf\Guzzle\CoroutineHandler;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* @deprecated since 3.1
|
||||
*/
|
||||
class HttpClient
|
||||
{
|
||||
protected Client $client;
|
||||
|
43
src/testing/src/TestCase.php
Normal file
43
src/testing/src/TestCase.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* This file is part of Hyperf.
|
||||
*
|
||||
* @link https://www.hyperf.io
|
||||
* @document https://hyperf.wiki
|
||||
* @contact group@hyperf.io
|
||||
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
|
||||
*/
|
||||
namespace Hyperf\Testing;
|
||||
|
||||
use Mockery as m;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
abstract class TestCase extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
use Concerns\InteractsWithContainer;
|
||||
use Concerns\MakesHttpRequests;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
if (! $this->container) {
|
||||
$this->refreshContainer();
|
||||
}
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$this->container = null;
|
||||
|
||||
try {
|
||||
m::close();
|
||||
} catch (Throwable) {
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user