From d802e8814843e39d07c0b78ffd02e0c7493602b6 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Thu, 7 Oct 2021 22:59:46 +0200 Subject: [PATCH] moved old Expectation in CoreExpectation.php and made Expectation.php a decorator for it --- src/CoreExpectation.php | 743 +++++++++++++++++++++++++++++ src/Expectation.php | 750 ++---------------------------- src/Factories/TestCaseFactory.php | 2 +- src/HigherOrderExpectation.php | 2 +- tests/Features/Exceptions.php | 6 - 5 files changed, 775 insertions(+), 728 deletions(-) create mode 100644 src/CoreExpectation.php diff --git a/src/CoreExpectation.php b/src/CoreExpectation.php new file mode 100644 index 00000000..4cd576fc --- /dev/null +++ b/src/CoreExpectation.php @@ -0,0 +1,743 @@ +value = $value; + } + + /** + * Asserts that two variables have the same type and + * value. Used on objects, it asserts that two + * variables reference the same object. + * + * @param mixed $expected + */ + public function toBe($expected): CoreExpectation + { + Assert::assertSame($expected, $this->value); + + return $this; + } + + /** + * Asserts that the value is empty. + */ + public function toBeEmpty(): CoreExpectation + { + Assert::assertEmpty($this->value); + + return $this; + } + + /** + * Asserts that the value is true. + */ + public function toBeTrue(): CoreExpectation + { + Assert::assertTrue($this->value); + + return $this; + } + + /** + * Asserts that the value is truthy. + */ + public function toBeTruthy(): CoreExpectation + { + Assert::assertTrue((bool) $this->value); + + return $this; + } + + /** + * Asserts that the value is false. + */ + public function toBeFalse(): CoreExpectation + { + Assert::assertFalse($this->value); + + return $this; + } + + /** + * Asserts that the value is falsy. + */ + public function toBeFalsy(): CoreExpectation + { + Assert::assertFalse((bool) $this->value); + + return $this; + } + + /** + * Asserts that the value is greater than $expected. + * + * @param int|float $expected + */ + public function toBeGreaterThan($expected): CoreExpectation + { + Assert::assertGreaterThan($expected, $this->value); + + return $this; + } + + /** + * Asserts that the value is greater than or equal to $expected. + * + * @param int|float $expected + */ + public function toBeGreaterThanOrEqual($expected): CoreExpectation + { + Assert::assertGreaterThanOrEqual($expected, $this->value); + + return $this; + } + + /** + * Asserts that the value is less than or equal to $expected. + * + * @param int|float $expected + */ + public function toBeLessThan($expected): CoreExpectation + { + Assert::assertLessThan($expected, $this->value); + + return $this; + } + + /** + * Asserts that the value is less than $expected. + * + * @param int|float $expected + */ + public function toBeLessThanOrEqual($expected): CoreExpectation + { + Assert::assertLessThanOrEqual($expected, $this->value); + + return $this; + } + + /** + * Asserts that $needle is an element of the value. + * + * @param mixed $needles + */ + public function toContain(...$needles): CoreExpectation + { + foreach ($needles as $needle) { + if (is_string($this->value)) { + Assert::assertStringContainsString($needle, $this->value); + } else { + Assert::assertContains($needle, $this->value); + } + } + + return $this; + } + + /** + * Asserts that the value starts with $expected. + */ + public function toStartWith(string $expected): CoreExpectation + { + Assert::assertStringStartsWith($expected, $this->value); + + return $this; + } + + /** + * Asserts that the value ends with $expected. + */ + public function toEndWith(string $expected): CoreExpectation + { + Assert::assertStringEndsWith($expected, $this->value); + + return $this; + } + + /** + * Asserts that $number matches value's Length. + */ + public function toHaveLength(int $number): CoreExpectation + { + if (is_string($this->value)) { + Assert::assertEquals($number, mb_strlen($this->value)); + + return $this; + } + + if (is_iterable($this->value)) { + return $this->toHaveCount($number); + } + + if (is_object($this->value)) { + if (method_exists($this->value, 'toArray')) { + $array = $this->value->toArray(); + } else { + $array = (array) $this->value; + } + + Assert::assertCount($number, $array); + + return $this; + } + + throw new BadMethodCallException('Expectation value length is not countable.'); + } + + /** + * Asserts that $count matches the number of elements of the value. + */ + public function toHaveCount(int $count): CoreExpectation + { + Assert::assertCount($count, $this->value); + + return $this; + } + + /** + * Asserts that the value contains the property $name. + * + * @param mixed $value + */ + public function toHaveProperty(string $name, $value = null): CoreExpectation + { + $this->toBeObject(); + + Assert::assertTrue(property_exists($this->value, $name)); + + if (func_num_args() > 1) { + /* @phpstan-ignore-next-line */ + Assert::assertEquals($value, $this->value->{$name}); + } + + return $this; + } + + /** + * Asserts that the value contains the provided properties $names. + * + * @param iterable $names + */ + public function toHaveProperties(iterable $names): CoreExpectation + { + foreach ($names as $name) { + $this->toHaveProperty($name); + } + + return $this; + } + + /** + * Asserts that two variables have the same value. + * + * @param mixed $expected + */ + public function toEqual($expected): CoreExpectation + { + Assert::assertEquals($expected, $this->value); + + return $this; + } + + /** + * Asserts that two variables have the same value. + * The contents of $expected and the $this->value are + * canonicalized before they are compared. For instance, when the two + * variables $expected and $this->value are arrays, then these arrays + * are sorted before they are compared. When $expected and $this->value + * are objects, each object is converted to an array containing all + * private, protected and public attributes. + * + * @param mixed $expected + */ + public function toEqualCanonicalizing($expected): CoreExpectation + { + Assert::assertEqualsCanonicalizing($expected, $this->value); + + return $this; + } + + /** + * Asserts that the absolute difference between the value and $expected + * is lower than $delta. + * + * @param mixed $expected + */ + public function toEqualWithDelta($expected, float $delta): CoreExpectation + { + Assert::assertEqualsWithDelta($expected, $this->value, $delta); + + return $this; + } + + /** + * Asserts that the value is one of the given values. + * + * @param iterable $values + */ + public function toBeIn(iterable $values): CoreExpectation + { + Assert::assertContains($this->value, $values); + + return $this; + } + + /** + * Asserts that the value is infinite. + */ + public function toBeInfinite(): CoreExpectation + { + Assert::assertInfinite($this->value); + + return $this; + } + + /** + * Asserts that the value is an instance of $class. + */ + public function toBeInstanceOf(string $class): CoreExpectation + { + /* @phpstan-ignore-next-line */ + Assert::assertInstanceOf($class, $this->value); + + return $this; + } + + /** + * Asserts that the value is an array. + */ + public function toBeArray(): CoreExpectation + { + Assert::assertIsArray($this->value); + + return $this; + } + + /** + * Asserts that the value is of type bool. + */ + public function toBeBool(): CoreExpectation + { + Assert::assertIsBool($this->value); + + return $this; + } + + /** + * Asserts that the value is of type callable. + */ + public function toBeCallable(): CoreExpectation + { + Assert::assertIsCallable($this->value); + + return $this; + } + + /** + * Asserts that the value is of type float. + */ + public function toBeFloat(): CoreExpectation + { + Assert::assertIsFloat($this->value); + + return $this; + } + + /** + * Asserts that the value is of type int. + */ + public function toBeInt(): CoreExpectation + { + Assert::assertIsInt($this->value); + + return $this; + } + + /** + * Asserts that the value is of type iterable. + */ + public function toBeIterable(): CoreExpectation + { + Assert::assertIsIterable($this->value); + + return $this; + } + + /** + * Asserts that the value is of type numeric. + */ + public function toBeNumeric(): CoreExpectation + { + Assert::assertIsNumeric($this->value); + + return $this; + } + + /** + * Asserts that the value is of type object. + */ + public function toBeObject(): CoreExpectation + { + Assert::assertIsObject($this->value); + + return $this; + } + + /** + * Asserts that the value is of type resource. + */ + public function toBeResource(): CoreExpectation + { + Assert::assertIsResource($this->value); + + return $this; + } + + /** + * Asserts that the value is of type scalar. + */ + public function toBeScalar(): CoreExpectation + { + Assert::assertIsScalar($this->value); + + return $this; + } + + /** + * Asserts that the value is of type string. + */ + public function toBeString(): CoreExpectation + { + Assert::assertIsString($this->value); + + return $this; + } + + /** + * Asserts that the value is a JSON string. + */ + public function toBeJson(): CoreExpectation + { + Assert::assertIsString($this->value); + Assert::assertJson($this->value); + + return $this; + } + + /** + * Asserts that the value is NAN. + */ + public function toBeNan(): CoreExpectation + { + Assert::assertNan($this->value); + + return $this; + } + + /** + * Asserts that the value is null. + */ + public function toBeNull(): CoreExpectation + { + Assert::assertNull($this->value); + + return $this; + } + + /** + * Asserts that the value array has the provided $key. + * + * @param string|int $key + * @param mixed $value + */ + public function toHaveKey($key, $value = null): CoreExpectation + { + if (is_object($this->value) && method_exists($this->value, 'toArray')) { + $array = $this->value->toArray(); + } else { + $array = (array) $this->value; + } + + try { + Assert::assertTrue(Arr::has($array, $key)); + + /* @phpstan-ignore-next-line */ + } catch (ExpectationFailedException $exception) { + throw new ExpectationFailedException("Failed asserting that an array has the key '$key'", $exception->getComparisonFailure()); + } + + if (func_num_args() > 1) { + Assert::assertEquals($value, Arr::get($array, $key)); + } + + return $this; + } + + /** + * Asserts that the value array has the provided $keys. + * + * @param array $keys + */ + public function toHaveKeys(array $keys): CoreExpectation + { + foreach ($keys as $key) { + $this->toHaveKey($key); + } + + return $this; + } + + /** + * Asserts that the value is a directory. + */ + public function toBeDirectory(): CoreExpectation + { + Assert::assertDirectoryExists($this->value); + + return $this; + } + + /** + * Asserts that the value is a directory and is readable. + */ + public function toBeReadableDirectory(): CoreExpectation + { + Assert::assertDirectoryIsReadable($this->value); + + return $this; + } + + /** + * Asserts that the value is a directory and is writable. + */ + public function toBeWritableDirectory(): CoreExpectation + { + Assert::assertDirectoryIsWritable($this->value); + + return $this; + } + + /** + * Asserts that the value is a file. + */ + public function toBeFile(): CoreExpectation + { + Assert::assertFileExists($this->value); + + return $this; + } + + /** + * Asserts that the value is a file and is readable. + */ + public function toBeReadableFile(): CoreExpectation + { + Assert::assertFileIsReadable($this->value); + + return $this; + } + + /** + * Asserts that the value is a file and is writable. + */ + public function toBeWritableFile(): CoreExpectation + { + Assert::assertFileIsWritable($this->value); + + return $this; + } + + /** + * Asserts that the value array matches the given array subset. + * + * @param array $array + */ + public function toMatchArray(array $array): CoreExpectation + { + if (is_object($this->value) && method_exists($this->value, 'toArray')) { + $valueAsArray = $this->value->toArray(); + } else { + $valueAsArray = (array) $this->value; + } + + foreach ($array as $key => $value) { + Assert::assertArrayHasKey($key, $valueAsArray); + + Assert::assertEquals( + $value, + $valueAsArray[$key], + sprintf( + 'Failed asserting that an array has a key %s with the value %s.', + $this->export($key), + $this->export($valueAsArray[$key]), + ), + ); + } + + return $this; + } + + /** + * Asserts that the value object matches a subset + * of the properties of an given object. + * + * @param array|object $object + */ + public function toMatchObject($object): CoreExpectation + { + foreach ((array) $object as $property => $value) { + Assert::assertTrue(property_exists($this->value, $property)); + + /* @phpstan-ignore-next-line */ + $propertyValue = $this->value->{$property}; + Assert::assertEquals( + $value, + $propertyValue, + sprintf( + 'Failed asserting that an object has a property %s with the value %s.', + $this->export($property), + $this->export($propertyValue), + ), + ); + } + + return $this; + } + + /** + * Asserts that the value matches a regular expression. + */ + public function toMatch(string $expression): CoreExpectation + { + Assert::assertMatchesRegularExpression($expression, $this->value); + + return $this; + } + + /** + * Asserts that the value matches a constraint. + */ + public function toMatchConstraint(Constraint $constraint): CoreExpectation + { + Assert::assertThat($this->value, $constraint); + + return $this; + } + + /** + * Asserts that executing value throws an exception. + * + * @param (Closure(Throwable): mixed)|string $exception + */ + public function toThrow($exception, string $exceptionMessage = null): CoreExpectation + { + $callback = NullClosure::create(); + + if ($exception instanceof Closure) { + $callback = $exception; + $parameters = (new ReflectionFunction($exception))->getParameters(); + + if (1 !== count($parameters)) { + throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.'); + } + + if (!($type = $parameters[0]->getType()) instanceof ReflectionNamedType) { + throw new InvalidArgumentException('The given closure\'s parameter must be type-hinted as the class string.'); + } + + $exception = $type->getName(); + } + + try { + ($this->value)(); + } catch (Throwable $e) { // @phpstan-ignore-line + if (!class_exists($exception)) { + Assert::assertStringContainsString($exception, $e->getMessage()); + + return $this; + } + + if ($exceptionMessage !== null) { + Assert::assertStringContainsString($exceptionMessage, $e->getMessage()); + } + + Assert::assertInstanceOf($exception, $e); + $callback($e); + + return $this; + } + + if (!class_exists($exception)) { + throw new ExpectationFailedException("Exception with message \"$exception\" not thrown."); + } + + throw new ExpectationFailedException("Exception \"$exception\" not thrown."); + } + + /** + * Exports the given value. + * + * @param mixed $value + */ + private function export($value): string + { + if ($this->exporter === null) { + $this->exporter = new Exporter(); + } + + return $this->exporter->export($value); + } +} diff --git a/src/Expectation.php b/src/Expectation.php index a973c760..5c64b3fd 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -5,19 +5,10 @@ declare(strict_types=1); namespace Pest; use BadMethodCallException; -use Closure; -use InvalidArgumentException; use Pest\Concerns\Extendable; use Pest\Concerns\RetrievesValues; -use Pest\Support\Arr; -use Pest\Support\NullClosure; use PHPUnit\Framework\Assert; -use PHPUnit\Framework\Constraint\Constraint; use PHPUnit\Framework\ExpectationFailedException; -use ReflectionFunction; -use ReflectionNamedType; -use SebastianBergmann\Exporter\Exporter; -use Throwable; /** * @internal @@ -26,6 +17,8 @@ use Throwable; * * @property Expectation $not Creates the opposite expectation. * @property Each $each Creates an expectation on each element on the traversable value. + * + * @mixin CoreExpectation */ final class Expectation { @@ -34,32 +27,17 @@ final class Expectation } use RetrievesValues; - /** - * The expectation value. - * - * @readonly - * - * @var mixed - */ - public $value; + /** @var CoreExpectation */ + private $coreExpectation; /** - * The exporter instance, if any. - * - * @readonly - * - * @var Exporter|null - */ - private $exporter; - - /** - * Creates a new expectation. + * Creates a new Expectation. * * @param TValue $value */ public function __construct($value) { - $this->value = $value; + $this->coreExpectation = new CoreExpectation($value); } /** @@ -147,6 +125,8 @@ final class Expectation * @template TSequenceValue * * @param callable(self, self): void|TSequenceValue ...$callbacks + * + * @noinspection PhpParamsInspection */ public function sequence(...$callbacks): Expectation { @@ -187,7 +167,7 @@ final class Expectation * * @template TMatchSubject of array-key * - * @param callable(): TMatchSubject|TMatchSubject $subject + * @param callable(): TMatchSubject|TMatchSubject $subject * @param array): mixed)|TValue> $expressions */ public function match($subject, array $expressions): Expectation @@ -198,7 +178,7 @@ final class Expectation return $subject; }; - $subject = $subject(); + $subject = $subject(); $matched = false; @@ -229,7 +209,7 @@ final class Expectation /** * Apply the callback if the given "condition" is falsy. * - * @param (callable(): bool)|bool $condition + * @param (callable(): bool)|bool $condition * @param callable(Expectation): mixed $callback */ public function unless($condition, callable $callback): Expectation @@ -246,7 +226,7 @@ final class Expectation /** * Apply the callback if the given "condition" is truthy. * - * @param (callable(): bool)|bool $condition + * @param (callable(): bool)|bool $condition * @param callable(Expectation): mixed $callback */ public function when($condition, callable $callback): Expectation @@ -264,692 +244,6 @@ final class Expectation return $this; } - /** - * Asserts that two variables have the same type and - * value. Used on objects, it asserts that two - * variables reference the same object. - * - * @param mixed $expected - */ - public function toBe($expected): Expectation - { - Assert::assertSame($expected, $this->value); - - return $this; - } - - /** - * Asserts that the value is empty. - */ - public function toBeEmpty(): Expectation - { - Assert::assertEmpty($this->value); - - return $this; - } - - /** - * Asserts that the value is true. - */ - public function toBeTrue(): Expectation - { - Assert::assertTrue($this->value); - - return $this; - } - - /** - * Asserts that the value is truthy. - */ - public function toBeTruthy(): Expectation - { - Assert::assertTrue((bool) $this->value); - - return $this; - } - - /** - * Asserts that the value is false. - */ - public function toBeFalse(): Expectation - { - Assert::assertFalse($this->value); - - return $this; - } - - /** - * Asserts that the value is falsy. - */ - public function toBeFalsy(): Expectation - { - Assert::assertFalse((bool) $this->value); - - return $this; - } - - /** - * Asserts that the value is greater than $expected. - * - * @param int|float $expected - */ - public function toBeGreaterThan($expected): Expectation - { - Assert::assertGreaterThan($expected, $this->value); - - return $this; - } - - /** - * Asserts that the value is greater than or equal to $expected. - * - * @param int|float $expected - */ - public function toBeGreaterThanOrEqual($expected): Expectation - { - Assert::assertGreaterThanOrEqual($expected, $this->value); - - return $this; - } - - /** - * Asserts that the value is less than or equal to $expected. - * - * @param int|float $expected - */ - public function toBeLessThan($expected): Expectation - { - Assert::assertLessThan($expected, $this->value); - - return $this; - } - - /** - * Asserts that the value is less than $expected. - * - * @param int|float $expected - */ - public function toBeLessThanOrEqual($expected): Expectation - { - Assert::assertLessThanOrEqual($expected, $this->value); - - return $this; - } - - /** - * Asserts that $needle is an element of the value. - * - * @param mixed $needles - */ - public function toContain(...$needles): Expectation - { - foreach ($needles as $needle) { - if (is_string($this->value)) { - Assert::assertStringContainsString($needle, $this->value); - } else { - Assert::assertContains($needle, $this->value); - } - } - - return $this; - } - - /** - * Asserts that the value starts with $expected. - */ - public function toStartWith(string $expected): Expectation - { - Assert::assertStringStartsWith($expected, $this->value); - - return $this; - } - - /** - * Asserts that the value ends with $expected. - */ - public function toEndWith(string $expected): Expectation - { - Assert::assertStringEndsWith($expected, $this->value); - - return $this; - } - - /** - * Asserts that $number matches value's Length. - */ - public function toHaveLength(int $number): Expectation - { - if (is_string($this->value)) { - Assert::assertEquals($number, mb_strlen($this->value)); - - return $this; - } - - if (is_iterable($this->value)) { - return $this->toHaveCount($number); - } - - if (is_object($this->value)) { - if (method_exists($this->value, 'toArray')) { - $array = $this->value->toArray(); - } else { - $array = (array) $this->value; - } - - Assert::assertCount($number, $array); - - return $this; - } - - throw new BadMethodCallException('Expectation value length is not countable.'); - } - - /** - * Asserts that $count matches the number of elements of the value. - */ - public function toHaveCount(int $count): Expectation - { - Assert::assertCount($count, $this->value); - - return $this; - } - - /** - * Asserts that the value contains the property $name. - * - * @param mixed $value - */ - public function toHaveProperty(string $name, $value = null): Expectation - { - $this->toBeObject(); - - Assert::assertTrue(property_exists($this->value, $name)); - - if (func_num_args() > 1) { - /* @phpstan-ignore-next-line */ - Assert::assertEquals($value, $this->value->{$name}); - } - - return $this; - } - - /** - * Asserts that the value contains the provided properties $names. - * - * @param iterable $names - */ - public function toHaveProperties(iterable $names): Expectation - { - foreach ($names as $name) { - $this->toHaveProperty($name); - } - - return $this; - } - - /** - * Asserts that two variables have the same value. - * - * @param mixed $expected - */ - public function toEqual($expected): Expectation - { - Assert::assertEquals($expected, $this->value); - - return $this; - } - - /** - * Asserts that two variables have the same value. - * The contents of $expected and the $this->value are - * canonicalized before they are compared. For instance, when the two - * variables $expected and $this->value are arrays, then these arrays - * are sorted before they are compared. When $expected and $this->value - * are objects, each object is converted to an array containing all - * private, protected and public attributes. - * - * @param mixed $expected - */ - public function toEqualCanonicalizing($expected): Expectation - { - Assert::assertEqualsCanonicalizing($expected, $this->value); - - return $this; - } - - /** - * Asserts that the absolute difference between the value and $expected - * is lower than $delta. - * - * @param mixed $expected - */ - public function toEqualWithDelta($expected, float $delta): Expectation - { - Assert::assertEqualsWithDelta($expected, $this->value, $delta); - - return $this; - } - - /** - * Asserts that the value is one of the given values. - * - * @param iterable $values - */ - public function toBeIn(iterable $values): Expectation - { - Assert::assertContains($this->value, $values); - - return $this; - } - - /** - * Asserts that the value is infinite. - */ - public function toBeInfinite(): Expectation - { - Assert::assertInfinite($this->value); - - return $this; - } - - /** - * Asserts that the value is an instance of $class. - * - * @param string $class - */ - public function toBeInstanceOf($class): Expectation - { - /* @phpstan-ignore-next-line */ - Assert::assertInstanceOf($class, $this->value); - - return $this; - } - - /** - * Asserts that the value is an array. - */ - public function toBeArray(): Expectation - { - Assert::assertIsArray($this->value); - - return $this; - } - - /** - * Asserts that the value is of type bool. - */ - public function toBeBool(): Expectation - { - Assert::assertIsBool($this->value); - - return $this; - } - - /** - * Asserts that the value is of type callable. - */ - public function toBeCallable(): Expectation - { - Assert::assertIsCallable($this->value); - - return $this; - } - - /** - * Asserts that the value is of type float. - */ - public function toBeFloat(): Expectation - { - Assert::assertIsFloat($this->value); - - return $this; - } - - /** - * Asserts that the value is of type int. - */ - public function toBeInt(): Expectation - { - Assert::assertIsInt($this->value); - - return $this; - } - - /** - * Asserts that the value is of type iterable. - */ - public function toBeIterable(): Expectation - { - Assert::assertIsIterable($this->value); - - return $this; - } - - /** - * Asserts that the value is of type numeric. - */ - public function toBeNumeric(): Expectation - { - Assert::assertIsNumeric($this->value); - - return $this; - } - - /** - * Asserts that the value is of type object. - */ - public function toBeObject(): Expectation - { - Assert::assertIsObject($this->value); - - return $this; - } - - /** - * Asserts that the value is of type resource. - */ - public function toBeResource(): Expectation - { - Assert::assertIsResource($this->value); - - return $this; - } - - /** - * Asserts that the value is of type scalar. - */ - public function toBeScalar(): Expectation - { - Assert::assertIsScalar($this->value); - - return $this; - } - - /** - * Asserts that the value is of type string. - */ - public function toBeString(): Expectation - { - Assert::assertIsString($this->value); - - return $this; - } - - /** - * Asserts that the value is a JSON string. - */ - public function toBeJson(): Expectation - { - Assert::assertIsString($this->value); - Assert::assertJson($this->value); - - return $this; - } - - /** - * Asserts that the value is NAN. - */ - public function toBeNan(): Expectation - { - Assert::assertNan($this->value); - - return $this; - } - - /** - * Asserts that the value is null. - */ - public function toBeNull(): Expectation - { - Assert::assertNull($this->value); - - return $this; - } - - /** - * Asserts that the value array has the provided $key. - * - * @param string|int $key - * @param mixed $value - */ - public function toHaveKey($key, $value = null): Expectation - { - if (is_object($this->value) && method_exists($this->value, 'toArray')) { - $array = $this->value->toArray(); - } else { - $array = (array) $this->value; - } - - try { - Assert::assertTrue(Arr::has($array, $key)); - - /* @phpstan-ignore-next-line */ - } catch (ExpectationFailedException $exception) { - throw new ExpectationFailedException("Failed asserting that an array has the key '$key'", $exception->getComparisonFailure()); - } - - if (func_num_args() > 1) { - Assert::assertEquals($value, Arr::get($array, $key)); - } - - return $this; - } - - /** - * Asserts that the value array has the provided $keys. - * - * @param array $keys - */ - public function toHaveKeys(array $keys): Expectation - { - foreach ($keys as $key) { - $this->toHaveKey($key); - } - - return $this; - } - - /** - * Asserts that the value is a directory. - */ - public function toBeDirectory(): Expectation - { - Assert::assertDirectoryExists($this->value); - - return $this; - } - - /** - * Asserts that the value is a directory and is readable. - */ - public function toBeReadableDirectory(): Expectation - { - Assert::assertDirectoryIsReadable($this->value); - - return $this; - } - - /** - * Asserts that the value is a directory and is writable. - */ - public function toBeWritableDirectory(): Expectation - { - Assert::assertDirectoryIsWritable($this->value); - - return $this; - } - - /** - * Asserts that the value is a file. - */ - public function toBeFile(): Expectation - { - Assert::assertFileExists($this->value); - - return $this; - } - - /** - * Asserts that the value is a file and is readable. - */ - public function toBeReadableFile(): Expectation - { - Assert::assertFileIsReadable($this->value); - - return $this; - } - - /** - * Asserts that the value is a file and is writable. - */ - public function toBeWritableFile(): Expectation - { - Assert::assertFileIsWritable($this->value); - - return $this; - } - - /** - * Asserts that the value array matches the given array subset. - * - * @param array $array - */ - public function toMatchArray($array): Expectation - { - if (is_object($this->value) && method_exists($this->value, 'toArray')) { - $valueAsArray = $this->value->toArray(); - } else { - $valueAsArray = (array) $this->value; - } - - foreach ($array as $key => $value) { - Assert::assertArrayHasKey($key, $valueAsArray); - - Assert::assertEquals( - $value, - $valueAsArray[$key], - sprintf( - 'Failed asserting that an array has a key %s with the value %s.', - $this->export($key), - $this->export($valueAsArray[$key]), - ), - ); - } - - return $this; - } - - /** - * Asserts that the value object matches a subset - * of the properties of an given object. - * - * @param array|object $object - */ - public function toMatchObject($object): Expectation - { - foreach ((array) $object as $property => $value) { - Assert::assertTrue(property_exists($this->value, $property)); - - /* @phpstan-ignore-next-line */ - $propertyValue = $this->value->{$property}; - Assert::assertEquals( - $value, - $propertyValue, - sprintf( - 'Failed asserting that an object has a property %s with the value %s.', - $this->export($property), - $this->export($propertyValue), - ), - ); - } - - return $this; - } - - /** - * Asserts that the value matches a regular expression. - */ - public function toMatch(string $expression): Expectation - { - Assert::assertMatchesRegularExpression($expression, $this->value); - - return $this; - } - - /** - * Asserts that the value matches a constraint. - */ - public function toMatchConstraint(Constraint $constraint): Expectation - { - Assert::assertThat($this->value, $constraint); - - return $this; - } - - /** - * Asserts that executing value throws an exception. - * - * @param (Closure(Throwable): mixed)|string $exception - */ - public function toThrow($exception, string $exceptionMessage = null): Expectation - { - $callback = NullClosure::create(); - - if ($exception instanceof Closure) { - $callback = $exception; - $parameters = (new ReflectionFunction($exception))->getParameters(); - - if (1 !== count($parameters)) { - throw new InvalidArgumentException('The given closure must have a single parameter type-hinted as the class string.'); - } - - if (!($type = $parameters[0]->getType()) instanceof ReflectionNamedType) { - throw new InvalidArgumentException('The given closure\'s parameter must be type-hinted as the class string.'); - } - - $exception = $type->getName(); - } - - try { - ($this->value)(); - } catch (Throwable $e) { // @phpstan-ignore-line - if (!class_exists($exception)) { - Assert::assertStringContainsString($exception, $e->getMessage()); - - return $this; - } - - if ($exceptionMessage !== null) { - Assert::assertStringContainsString($exceptionMessage, $e->getMessage()); - } - - Assert::assertInstanceOf($exception, $e); - $callback($e); - - return $this; - } - - if (!class_exists($exception)) { - throw new ExpectationFailedException("Exception with message \"{$exception}\" not thrown."); - } - - throw new ExpectationFailedException("Exception \"{$exception}\" not thrown."); - } - - /** - * Exports the given value. - * - * @param mixed $value - */ - private function export($value): string - { - if ($this->exporter === null) { - $this->exporter = new Exporter(); - } - - return $this->exporter->export($value); - } - /** * Dynamically handle calls to the class or * creates a new higher order expectation. @@ -960,7 +254,14 @@ final class Expectation */ public function __call(string $method, array $parameters) { - if (!static::hasExtend($method)) { + if (method_exists($this->coreExpectation, $method)) { + //@phpstan-ignore-next-line + $this->coreExpectation = $this->coreExpectation->{$method}(...$parameters); + + return $this; + } + + if (!self::hasExtend($method)) { /* @phpstan-ignore-next-line */ return new HigherOrderExpectation($this, $this->value->$method(...$parameters)); } @@ -976,11 +277,20 @@ final class Expectation */ public function __get(string $name) { - if (!method_exists($this, $name) && !static::hasExtend($name)) { + if ($name === 'value') { + return $this->coreExpectation->value; + } + + if (!method_exists($this, $name) && !method_exists($this->coreExpectation, $name) && !self::hasExtend($name)) { return new HigherOrderExpectation($this, $this->retrieve($name, $this->value)); } /* @phpstan-ignore-next-line */ return $this->{$name}(); } + + public static function hasMethod(string $name): bool + { + return method_exists(CoreExpectation::class, $name); + } } diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index 42c3ca6e..bc75f5c1 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -144,8 +144,8 @@ final class TestCaseFactory * @return mixed */ $test = function () use ($chains, $proxies, $factoryTest) { - $chains->chain($this); $proxies->proxy($this); + $chains->chain($this); /* @phpstan-ignore-next-line */ return call_user_func(Closure::bind($factoryTest, $this, get_class($this)), ...func_get_args()); diff --git a/src/HigherOrderExpectation.php b/src/HigherOrderExpectation.php index e436da0c..4974c99f 100644 --- a/src/HigherOrderExpectation.php +++ b/src/HigherOrderExpectation.php @@ -113,7 +113,7 @@ final class HigherOrderExpectation */ private function expectationHasMethod(string $name): bool { - return method_exists($this->original, $name) || $this->original::hasExtend($name); + return method_exists($this->original, $name) || $this->original::hasMethod($name) || $this->original::hasExtend($name); } /** diff --git a/tests/Features/Exceptions.php b/tests/Features/Exceptions.php index 22d9d4ea..9970c2a9 100644 --- a/tests/Features/Exceptions.php +++ b/tests/Features/Exceptions.php @@ -1,7 +1,5 @@ expectException(InvalidArgumentException::class); @@ -39,7 +37,3 @@ it('can just define the message if given condition is true', function () { it('can just define the message if given condition is 1', function () { throw new Exception('Something bad happened'); })->throwsIf(1, 'Something bad happened'); - -it('can handle a skipped test if it is trying to catch an exception', function () { - expect(1)->toBe(2); -})->throws(ExpectationFailedException::class)->skip('this test should be skipped')->only();