⚙️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:
Deeka Wong 2023-05-16 16:39:01 +08:00 committed by 李铭昕
parent eba329e1c2
commit ce516ea2a3
21 changed files with 2826 additions and 1 deletions

View File

@ -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",

View File

@ -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#'

View File

@ -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": {

View 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);
}
}

View 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 . ',',
];
}
}

View File

@ -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;

View 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())());
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View 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
)
);
}
}

View 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;
}
}

View 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);
}

View 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);
}

View 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);
}

View 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);
}

View 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;
}
}

View 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);
}
}

View 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}.";
}
}

View File

@ -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;

View 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) {
}
}
}