From c07cd8c252204f7764442cb806f21bb03f7a2f70 Mon Sep 17 00:00:00 2001 From: Owen Voke Date: Tue, 15 Jun 2021 17:07:11 +0100 Subject: [PATCH] feat: move Expectations API out of external plugin --- composer.json | 1 - src/Concerns/Expectations.php | 23 + src/Concerns/Extendable.php | 54 ++ src/Each.php | 77 ++ src/Expectation.php | 714 ++++++++++++++++++ src/Factories/TestCaseFactory.php | 1 + src/Functions.php | 18 + src/HigherOrderExpectation.php | 147 ++++ src/OppositeExpectation.php | 99 +++ src/Support/Extendable.php | 33 + tests/.snapshots/success.txt | 274 ++++++- tests/Features/Expect/HigherOrder/methods.php | 100 +++ .../HigherOrder/methodsAndProperties.php | 50 ++ .../Expect/HigherOrder/properties.php | 75 ++ tests/Features/Expect/each.php | 89 +++ tests/Features/Expect/extend.php | 29 + tests/Features/Expect/not.php | 10 + tests/Features/Expect/ray.php | 5 + tests/Features/Expect/sequence.php | 46 ++ tests/Features/Expect/toBe.php | 20 + tests/Features/Expect/toBeArray.php | 16 + tests/Features/Expect/toBeBool.php | 16 + tests/Features/Expect/toBeCallable.php | 18 + tests/Features/Expect/toBeDirectory.php | 17 + tests/Features/Expect/toBeEmpty.php | 18 + tests/Features/Expect/toBeFalse.php | 15 + tests/Features/Expect/toBeFile.php | 23 + tests/Features/Expect/toBeFloat.php | 16 + tests/Features/Expect/toBeGreatherThan.php | 16 + .../Expect/toBeGreatherThanOrEqual.php | 16 + tests/Features/Expect/toBeInfinite.php | 16 + tests/Features/Expect/toBeInstanceOf.php | 16 + tests/Features/Expect/toBeInt.php | 16 + tests/Features/Expect/toBeIterable.php | 23 + tests/Features/Expect/toBeJson.php | 17 + tests/Features/Expect/toBeLessThan.php | 16 + tests/Features/Expect/toBeLessThanOrEqual.php | 16 + tests/Features/Expect/toBeNAN.php | 16 + tests/Features/Expect/toBeNull.php | 16 + tests/Features/Expect/toBeNumeric.php | 16 + tests/Features/Expect/toBeObject.php | 16 + .../Features/Expect/toBeReadableDirectory.php | 15 + tests/Features/Expect/toBeReadableFile.php | 23 + tests/Features/Expect/toBeResource.php | 22 + tests/Features/Expect/toBeScalar.php | 15 + tests/Features/Expect/toBeString.php | 16 + tests/Features/Expect/toBeTrue.php | 15 + .../Features/Expect/toBeWritableDirectory.php | 15 + tests/Features/Expect/toBeWritableFile.php | 23 + tests/Features/Expect/toContain.php | 19 + tests/Features/Expect/toEndWith.php | 15 + tests/Features/Expect/toEqual.php | 15 + .../Features/Expect/toEqualCanonicalizing.php | 16 + tests/Features/Expect/toEqualWithDelta.php | 15 + tests/Features/Expect/toHaveCount.php | 15 + tests/Features/Expect/toHaveKey.php | 15 + tests/Features/Expect/toHaveKeys.php | 15 + tests/Features/Expect/toHaveProperty.php | 22 + tests/Features/Expect/toMatch.php | 15 + tests/Features/Expect/toMatchArray.php | 31 + tests/Features/Expect/toMatchConstraint.php | 16 + tests/Features/Expect/toMatchObject.php | 31 + tests/Features/Expect/toStartWith.php | 15 + 63 files changed, 2617 insertions(+), 2 deletions(-) create mode 100644 src/Concerns/Expectations.php create mode 100644 src/Concerns/Extendable.php create mode 100644 src/Each.php create mode 100644 src/Expectation.php create mode 100644 src/HigherOrderExpectation.php create mode 100644 src/OppositeExpectation.php create mode 100644 src/Support/Extendable.php create mode 100644 tests/Features/Expect/HigherOrder/methods.php create mode 100644 tests/Features/Expect/HigherOrder/methodsAndProperties.php create mode 100644 tests/Features/Expect/HigherOrder/properties.php create mode 100644 tests/Features/Expect/each.php create mode 100644 tests/Features/Expect/extend.php create mode 100644 tests/Features/Expect/not.php create mode 100644 tests/Features/Expect/ray.php create mode 100644 tests/Features/Expect/sequence.php create mode 100644 tests/Features/Expect/toBe.php create mode 100644 tests/Features/Expect/toBeArray.php create mode 100644 tests/Features/Expect/toBeBool.php create mode 100644 tests/Features/Expect/toBeCallable.php create mode 100644 tests/Features/Expect/toBeDirectory.php create mode 100644 tests/Features/Expect/toBeEmpty.php create mode 100644 tests/Features/Expect/toBeFalse.php create mode 100644 tests/Features/Expect/toBeFile.php create mode 100644 tests/Features/Expect/toBeFloat.php create mode 100644 tests/Features/Expect/toBeGreatherThan.php create mode 100644 tests/Features/Expect/toBeGreatherThanOrEqual.php create mode 100644 tests/Features/Expect/toBeInfinite.php create mode 100644 tests/Features/Expect/toBeInstanceOf.php create mode 100644 tests/Features/Expect/toBeInt.php create mode 100644 tests/Features/Expect/toBeIterable.php create mode 100644 tests/Features/Expect/toBeJson.php create mode 100644 tests/Features/Expect/toBeLessThan.php create mode 100644 tests/Features/Expect/toBeLessThanOrEqual.php create mode 100644 tests/Features/Expect/toBeNAN.php create mode 100644 tests/Features/Expect/toBeNull.php create mode 100644 tests/Features/Expect/toBeNumeric.php create mode 100644 tests/Features/Expect/toBeObject.php create mode 100644 tests/Features/Expect/toBeReadableDirectory.php create mode 100644 tests/Features/Expect/toBeReadableFile.php create mode 100644 tests/Features/Expect/toBeResource.php create mode 100644 tests/Features/Expect/toBeScalar.php create mode 100644 tests/Features/Expect/toBeString.php create mode 100644 tests/Features/Expect/toBeTrue.php create mode 100644 tests/Features/Expect/toBeWritableDirectory.php create mode 100644 tests/Features/Expect/toBeWritableFile.php create mode 100644 tests/Features/Expect/toContain.php create mode 100644 tests/Features/Expect/toEndWith.php create mode 100644 tests/Features/Expect/toEqual.php create mode 100644 tests/Features/Expect/toEqualCanonicalizing.php create mode 100644 tests/Features/Expect/toEqualWithDelta.php create mode 100644 tests/Features/Expect/toHaveCount.php create mode 100644 tests/Features/Expect/toHaveKey.php create mode 100644 tests/Features/Expect/toHaveKeys.php create mode 100644 tests/Features/Expect/toHaveProperty.php create mode 100644 tests/Features/Expect/toMatch.php create mode 100644 tests/Features/Expect/toMatchArray.php create mode 100644 tests/Features/Expect/toMatchConstraint.php create mode 100644 tests/Features/Expect/toMatchObject.php create mode 100644 tests/Features/Expect/toStartWith.php diff --git a/composer.json b/composer.json index e075f4e3..23f64dd4 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,6 @@ "php": "^7.3 || ^8.0", "nunomaduro/collision": "^5.0", "pestphp/pest-plugin": "^1.0", - "pestphp/pest-plugin-expectations": "^1.6", "phpunit/phpunit": ">= 9.3.7 <= 9.5.5" }, "autoload": { diff --git a/src/Concerns/Expectations.php b/src/Concerns/Expectations.php new file mode 100644 index 00000000..27264f93 --- /dev/null +++ b/src/Concerns/Expectations.php @@ -0,0 +1,23 @@ + + */ + private static $extends = []; + + /** + * Register a custom extend. + */ + public static function extend(string $name, Closure $extend): void + { + static::$extends[$name] = $extend; + } + + /** + * Checks if extend is registered. + */ + public static function hasExtend(string $name): bool + { + return array_key_exists($name, static::$extends); + } + + /** + * Dynamically handle calls to the class. + * + * @param array $parameters + * + * @return mixed + */ + public function __call(string $method, array $parameters) + { + if (!static::hasExtend($method)) { + return new HigherOrderExpectation($this, $method, $parameters); + } + + /** @var Closure $extend */ + $extend = static::$extends[$method]->bindTo($this, static::class); + + return $extend(...$parameters); + } +} diff --git a/src/Each.php b/src/Each.php new file mode 100644 index 00000000..f02a8e1e --- /dev/null +++ b/src/Each.php @@ -0,0 +1,77 @@ +original = $original; + } + + /** + * Creates a new expectation. + * + * @param mixed $value + */ + public function and($value): Expectation + { + return $this->original->and($value); + } + + /** + * Creates the opposite expectation for the value. + */ + public function not(): Each + { + $this->opposite = true; + + return $this; + } + + /** + * Dynamically calls methods on the class with the given arguments on each item. + * + * @param array $arguments + */ + public function __call(string $name, array $arguments): Each + { + foreach ($this->original->value as $item) { + /* @phpstan-ignore-next-line */ + $this->opposite ? expect($item)->not()->$name(...$arguments) : expect($item)->$name(...$arguments); + } + + $this->opposite = false; + + return $this; + } + + /** + * Dynamically calls methods on the class without any arguments on each item. + */ + public function __get(string $name): Each + { + /* @phpstan-ignore-next-line */ + return $this->$name(); + } +} diff --git a/src/Expectation.php b/src/Expectation.php new file mode 100644 index 00000000..e35acc5f --- /dev/null +++ b/src/Expectation.php @@ -0,0 +1,714 @@ +value = $value; + } + + /** + * Creates a new expectation. + * + * @param mixed $value + */ + public function and($value): Expectation + { + return new self($value); + } + + /** + * Dump the expectation value and end the script. + * + * @param mixed $arguments + * + * @return never + */ + public function dd(...$arguments): void + { + if (function_exists('dd')) { + dd($this->value, ...$arguments); + } + + var_dump($this->value); + + exit(1); + } + + /** + * Send the expectation value to Ray along with all given arguments. + * + * @param mixed $arguments + */ + public function ray(...$arguments): self + { + if (function_exists('ray')) { + // @phpstan-ignore-next-line + ray($this->value, ...$arguments); + } + + return $this; + } + + /** + * Creates the opposite expectation for the value. + */ + public function not(): OppositeExpectation + { + return new OppositeExpectation($this); + } + + /** + * Creates an expectation on each item of the iterable "value". + */ + public function each(callable $callback = null): Each + { + if (!is_iterable($this->value)) { + throw new BadMethodCallException('Expectation value is not iterable.'); + } + + if (is_callable($callback)) { + foreach ($this->value as $item) { + $callback(expect($item)); + } + } + + return new Each($this); + } + + /** + * Allows you to specify a sequential set of expectations for each item in a iterable "value". + */ + public function sequence(callable ...$callbacks): Expectation + { + if (!is_iterable($this->value)) { + throw new BadMethodCallException('Expectation value is not iterable.'); + } + + $value = is_array($this->value) ? $this->value : iterator_to_array($this->value); + $keys = array_keys($value); + $values = array_values($value); + + $index = 0; + + while (count($callbacks) < count($values)) { + $callbacks[] = $callbacks[$index]; + $index = $index < count($values) - 1 ? $index + 1 : 0; + } + + foreach ($values as $key => $item) { + call_user_func($callbacks[$key], expect($item), expect($keys[$key])); + } + + 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 false. + */ + public function toBeFalse(): Expectation + { + Assert::assertFalse($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 $needle + */ + public function toContain($needle): Expectation + { + 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 $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 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 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; + } + + Assert::assertArrayHasKey($key, $array); + + if (func_num_args() > 1) { + Assert::assertEquals($value, $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; + } + + /** + * 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 calls methods on the class without any arguments + * or creates a new higher order expectation. + * + * @return Expectation|HigherOrderExpectation + */ + public function __get(string $name) + { + if (!method_exists($this, $name) && !static::hasExtend($name)) { + return new HigherOrderExpectation($this, $name); + } + + /* @phpstan-ignore-next-line */ + return $this->{$name}(); + } +} diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index 4094d411..b78c67bf 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -80,6 +80,7 @@ final class TestCaseFactory */ public $traits = [ Concerns\TestCase::class, + Concerns\Expectations::class, ]; /** diff --git a/src/Functions.php b/src/Functions.php index e59641dc..e7a230dc 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -3,15 +3,33 @@ declare(strict_types=1); use Pest\Datasets; +use Pest\Expectation; use Pest\PendingObjects\AfterEachCall; use Pest\PendingObjects\BeforeEachCall; use Pest\PendingObjects\TestCall; use Pest\PendingObjects\UsesCall; use Pest\Support\Backtrace; +use Pest\Support\Extendable; use Pest\Support\HigherOrderTapProxy; use Pest\TestSuite; use PHPUnit\Framework\TestCase; +/** + * Creates a new expectation. + * + * @param mixed $value the Value + * + * @return Expectation|Extendable + */ +function expect($value = null) +{ + if (func_num_args() === 0) { + return new Extendable(Expectation::class); + } + + return new Expectation($value); +} + if (!function_exists('beforeAll')) { /** * Runs the given closure before all tests in the current file. diff --git a/src/HigherOrderExpectation.php b/src/HigherOrderExpectation.php new file mode 100644 index 00000000..7907ccb0 --- /dev/null +++ b/src/HigherOrderExpectation.php @@ -0,0 +1,147 @@ +|null $parameters + * @phpstan-ignore-next-line + */ + public function __construct(Expectation $original, string $name, ?array $parameters = null) + { + $this->original = $original; + $this->name = $name; + + $this->expectation = $this->expect( + is_null($parameters) ? $this->getPropertyValue() : $this->getMethodValue($parameters) + ); + } + + /** + * Retrieves the property value from the original expectation. + * + * @return mixed + */ + private function getPropertyValue() + { + if (is_array($this->original->value)) { + return $this->original->value[$this->name]; + } + + // @phpstan-ignore-next-line + return $this->original->value->{$this->name}; + } + + /** + * Retrieves the value of the method from the original expectation. + * + * @param array $arguments + * + * @return mixed + */ + private function getMethodValue(array $arguments) + { + // @phpstan-ignore-next-line + return $this->original->value->{$this->name}(...$arguments); + } + + /** + * Creates the opposite expectation for the value. + */ + public function not(): HigherOrderExpectation + { + $this->opposite = !$this->opposite; + + return $this; + } + + /** + * Dynamically calls methods on the class with the given arguments. + * + * @param array $arguments + */ + public function __call(string $name, array $arguments): self + { + if (!$this->originalHasMethod($name)) { + return new self($this->original, $name, $arguments); + } + + return $this->performAssertion($name, $arguments); + } + + /** + * Accesses properties in the value or in the expectation. + */ + public function __get(string $name): self + { + if ($name === 'not') { + return $this->not(); + } + + if (!$this->originalHasMethod($name)) { + return new self($this->original, $name); + } + + return $this->performAssertion($name, []); + } + + /** + * Determines if the original expectation has the given method name. + */ + private function originalHasMethod(string $name): bool + { + return method_exists($this->original, $name) || $this->original::hasExtend($name); + } + + /** + * Performs the given assertion with the current expectation. + * + * @param array $arguments + */ + private function performAssertion(string $name, array $arguments): self + { + $expectation = $this->opposite + ? $this->expectation->not() + : $this->expectation; + + $this->expectation = $expectation->{$name}(...$arguments); // @phpstan-ignore-line + + $this->opposite = false; + + return $this; + } +} diff --git a/src/OppositeExpectation.php b/src/OppositeExpectation.php new file mode 100644 index 00000000..0473e2b6 --- /dev/null +++ b/src/OppositeExpectation.php @@ -0,0 +1,99 @@ +original = $original; + } + + /** + * Asserts that the value array not has the provided $keys. + * + * @param array $keys + */ + public function toHaveKeys(array $keys): Expectation + { + foreach ($keys as $key) { + try { + $this->original->toHaveKey($key); + } catch (ExpectationFailedException $e) { + continue; + } + + $this->throwExpectationFailedException('toHaveKey', [$key]); + } + + return $this->original; + } + + /** + * Handle dynamic method calls into the original expectation. + * + * @param array $arguments + */ + public function __call(string $name, array $arguments): Expectation + { + try { + /* @phpstan-ignore-next-line */ + $this->original->{$name}(...$arguments); + } catch (ExpectationFailedException $e) { + return $this->original; + } + + // @phpstan-ignore-next-line + $this->throwExpectationFailedException($name, $arguments); + } + + /** + * Handle dynamic properties gets into the original expectation. + */ + public function __get(string $name): Expectation + { + try { + /* @phpstan-ignore-next-line */ + $this->original->{$name}; + } catch (ExpectationFailedException $e) { + return $this->original; + } + + // @phpstan-ignore-next-line + $this->throwExpectationFailedException($name); + } + + /** + * Creates a new expectation failed exception with a nice readable message. + * + * @param array $arguments + */ + private function throwExpectationFailedException(string $name, array $arguments = []): void + { + $exporter = new Exporter(); + + $toString = function ($argument) use ($exporter): string { + return $exporter->shortenedExport($argument); + }; + + throw new ExpectationFailedException(sprintf('Expecting %s not %s %s.', $toString($this->original->value), strtolower((string) preg_replace('/(?extendableClass = $extendableClass; + } + + /** + * Register a custom extend. + */ + public function extend(string $name, Closure $extend): void + { + $this->extendableClass::extend($name, $extend); + } +} diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 0bb41268..9f57d4fa 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -102,6 +102,278 @@ ✓ it catch exceptions ✓ it catch exceptions and messages + PASS Tests\Features\Expect\HigherOrder\methods + ✓ it can access methods + ✓ it can access multiple methods + ✓ it works with not + ✓ it can accept arguments + ✓ it works with each + ✓ it works inside of each + ✓ it works with sequence + ✓ it can compose complex expectations + + PASS Tests\Features\Expect\HigherOrder\methodsAndProperties + ✓ it can access methods and properties + + PASS Tests\Features\Expect\HigherOrder\properties + ✓ it allows properties to be accessed from the value + ✓ it can access multiple properties from the value + ✓ it works with not + ✓ it works with each + ✓ it works inside of each + ✓ it works with sequence + ✓ it can compose complex expectations + ✓ it works with objects + + PASS Tests\Features\Expect\each + ✓ an exception is thrown if the the type is not iterable + ✓ it expects on each item + ✓ it chains expectations on each item + ✓ opposite expectations on each item + ✓ chained opposite and non-opposite expectations + ✓ it can add expectations via "and" + ✓ it accepts callables + + PASS Tests\Features\Expect\extend + ✓ it macros true is true + ✓ it macros false is not true + ✓ it macros true is true with argument + ✓ it macros false is not true with argument + + PASS Tests\Features\Expect\not + ✓ not property calls + + PASS Tests\Features\Expect\ray + ✓ ray calls do not fail when ray is not installed + + PASS Tests\Features\Expect\sequence + ✓ an exception is thrown if the the type is not iterable + ✓ allows for sequences of checks to be run on iterable data + ✓ loops back to the start if it runs out of sequence items + ✓ it works if the number of items in the iterable is smaller than the number of expectations + ✓ it works with associative arrays + + PASS Tests\Features\Expect\toBe + ✓ strict comparisons + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeArray + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeBool + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeCallable + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeDirectory + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeEmpty + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeFalse + ✓ strict comparisons + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeFile + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeFloat + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeGreatherThan + ✓ passes + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeGreatherThanOrEqual + ✓ passes + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeInfinite + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeInstanceOf + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeInt + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeIterable + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeJson + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeLessThan + ✓ passes + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeLessThanOrEqual + ✓ passes + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeNAN + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeNull + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeNumeric + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeObject + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeReadableDirectory + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeReadableFile + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeResource + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeScalar + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeString + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeTrue + ✓ strict comparisons + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeWritableDirectory + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toBeWritableFile + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toContain + ✓ passes strings + ✓ passes arrays + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toEndWith + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toEqual + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toEqualCanonicalizing + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toEqualWithDelta + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toHaveCount + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toHaveKey + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toHaveKeys + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toHaveProperty + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toMatch + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toMatchArray + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toMatchConstraint + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toMatchObject + ✓ pass + ✓ failures + ✓ not failures + + PASS Tests\Features\Expect\toStartWith + ✓ pass + ✓ failures + ✓ not failures + PASS Tests\Features\Helpers ✓ it can set/get properties on $this ✓ it throws error if property do not exist @@ -282,5 +554,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 4 incompleted, 7 skipped, 172 passed + Tests: 4 incompleted, 7 skipped, 340 passed \ No newline at end of file diff --git a/tests/Features/Expect/HigherOrder/methods.php b/tests/Features/Expect/HigherOrder/methods.php new file mode 100644 index 00000000..7f340f4a --- /dev/null +++ b/tests/Features/Expect/HigherOrder/methods.php @@ -0,0 +1,100 @@ +name()->toBeString()->toEqual('Has Methods'); +}); + +it('can access multiple methods', function () { + expect(new HasMethods()) + ->name()->toBeString()->toEqual('Has Methods') + ->quantity()->toBeInt()->toEqual(20); +}); + +it('works with not', function () { + expect(new HasMethods()) + ->name()->not->toEqual('world')->toEqual('Has Methods') + ->quantity()->toEqual(20)->not()->toEqual('bar')->not->toBeNull; +}); + +it('can accept arguments', function () { + expect(new HasMethods()) + ->multiply(5, 4)->toBeInt->toEqual(20); +}); + +it('works with each', function () { + expect(new HasMethods()) + ->attributes()->toBeArray->each->not()->toBeNull + ->attributes()->each(function ($attribute) { + $attribute->not->toBeNull(); + }); +}); + +it('works inside of each', function () { + expect(new HasMethods()) + ->books()->each(function ($book) { + $book->title->not->toBeNull->cost->toBeGreaterThan(19); + }); +}); + +it('works with sequence', function () { + expect(new HasMethods()) + ->books()->sequence( + function ($book) { $book->title->toEqual('Foo')->cost->toEqual(20); }, + function ($book) { $book->title->toEqual('Bar')->cost->toEqual(30); }, + ); +}); + +it('can compose complex expectations', function () { + expect(new HasMethods()) + ->toBeObject() + ->name()->toEqual('Has Methods')->not()->toEqual('bar') + ->quantity()->not->toEqual('world')->toEqual(20)->toBeInt + ->multiply(3, 4)->not->toBeString->toEqual(12) + ->attributes()->toBeArray() + ->books()->toBeArray->each->not->toBeEmpty + ->books()->sequence( + function ($book) { $book->title->toEqual('Foo')->cost->toEqual(20); }, + function ($book) { $book->title->toEqual('Bar')->cost->toEqual(30); }, + ); +}); + +class HasMethods +{ + public function name() + { + return 'Has Methods'; + } + + public function quantity() + { + return 20; + } + + public function multiply($x, $y) + { + return $x * $y; + } + + public function attributes() + { + return [ + 'name' => $this->name(), + 'quantity' => $this->quantity(), + ]; + } + + public function books() + { + return [ + [ + 'title' => 'Foo', + 'cost' => 20, + ], + [ + 'title' => 'Bar', + 'cost' => 30, + ], + ]; + } +} diff --git a/tests/Features/Expect/HigherOrder/methodsAndProperties.php b/tests/Features/Expect/HigherOrder/methodsAndProperties.php new file mode 100644 index 00000000..34ce09b4 --- /dev/null +++ b/tests/Features/Expect/HigherOrder/methodsAndProperties.php @@ -0,0 +1,50 @@ +name->toEqual('Has Methods and Properties')->not()->toEqual('bar') + ->multiply(3, 4)->not->toBeString->toEqual(12) + ->posts->each(function ($post) { + $post->is_published->toBeTrue; + })->books()->toBeArray() + ->posts->toBeArray->each->not->toBeEmpty + ->books()->sequence( + function ($book) { $book->title->toEqual('Foo')->cost->toEqual(20); }, + function ($book) { $book->title->toEqual('Bar')->cost->toEqual(30); }, + ); +}); + +class HasMethodsAndProperties +{ + public $name = 'Has Methods and Properties'; + + public $posts = [ + [ + 'is_published' => true, + 'title' => 'Foo', + ], + [ + 'is_published' => true, + 'title' => 'Bar', + ], + ]; + + public function books() + { + return [ + [ + 'title' => 'Foo', + 'cost' => 20, + ], + [ + 'title' => 'Bar', + 'cost' => 30, + ], + ]; + } + + public function multiply($x, $y) + { + return $x * $y; + } +} diff --git a/tests/Features/Expect/HigherOrder/properties.php b/tests/Features/Expect/HigherOrder/properties.php new file mode 100644 index 00000000..c73e3d03 --- /dev/null +++ b/tests/Features/Expect/HigherOrder/properties.php @@ -0,0 +1,75 @@ + 1])->foo->toBeInt()->toEqual(1); +}); + +it('can access multiple properties from the value', function () { + expect(['foo' => 'bar', 'hello' => 'world']) + ->foo->toBeString()->toEqual('bar') + ->hello->toBeString()->toEqual('world'); +}); + +it('works with not', function () { + expect(['foo' => 'bar', 'hello' => 'world']) + ->foo->not->not->toEqual('bar') + ->foo->not->toEqual('world')->toEqual('bar') + ->hello->toEqual('world')->not()->toEqual('bar')->not->toBeNull; +}); + +it('works with each', function () { + expect(['numbers' => [1, 2, 3, 4], 'words' => ['hey', 'there']]) + ->numbers->toEqual([1, 2, 3, 4])->each->toBeInt->toBeLessThan(5) + ->words->each(function ($word) { + $word->toBeString()->not->toBeInt(); + }); +}); + +it('works inside of each', function () { + expect(['books' => [['title' => 'Foo', 'cost' => 20], ['title' => 'Bar', 'cost' => 30]]]) + ->books->each(function ($book) { + $book->title->not->toBeNull->cost->toBeGreaterThan(19); + }); +}); + +it('works with sequence', function () { + expect(['books' => [['title' => 'Foo', 'cost' => 20], ['title' => 'Bar', 'cost' => 30]]]) + ->books->sequence( + function ($book) { $book->title->toEqual('Foo')->cost->toEqual(20); }, + function ($book) { $book->title->toEqual('Bar')->cost->toEqual(30); }, + ); +}); + +it('can compose complex expectations', function () { + expect(['foo' => 'bar', 'numbers' => [1, 2, 3, 4]]) + ->toContain('bar')->toBeArray() + ->numbers->toEqual([1, 2, 3, 4])->not()->toEqual('bar')->each->toBeInt + ->foo->not->toEqual('world')->toEqual('bar') + ->numbers->toBeArray(); +}); + +it('works with objects', function () { + expect(new HasProperties()) + ->name->toEqual('foo')->not->toEqual('world') + ->posts->toHaveCount(2)->each(function ($post) { $post->is_published->toBeTrue(); }) + ->posts->sequence( + function ($post) { $post->title->toEqual('Foo'); }, + function ($post) { $post->title->toEqual('Bar'); }, + ); +}); + +class HasProperties +{ + public $name = 'foo'; + + public $posts = [ + [ + 'is_published' => true, + 'title' => 'Foo', + ], + [ + 'is_published' => true, + 'title' => 'Bar', + ], + ]; +} diff --git a/tests/Features/Expect/each.php b/tests/Features/Expect/each.php new file mode 100644 index 00000000..0f26a974 --- /dev/null +++ b/tests/Features/Expect/each.php @@ -0,0 +1,89 @@ +each()->toEqual('Foobar'); +})->throws(BadMethodCallException::class, 'Expectation value is not iterable.'); + +it('expects on each item', function () { + expect([1, 1, 1]) + ->each() + ->toEqual(1); + + expect(static::getCount())->toBe(3); // + 1 assertion + + expect([1, 1, 1]) + ->each + ->toEqual(1); + + expect(static::getCount())->toBe(7); +}); + +it('chains expectations on each item', function () { + expect([1, 1, 1]) + ->each() + ->toBeInt() + ->toEqual(1); + + expect(static::getCount())->toBe(6); // + 1 assertion + + expect([2, 2, 2]) + ->each + ->toBeInt + ->toEqual(2); + + expect(static::getCount())->toBe(13); +}); + +test('opposite expectations on each item', function () { + expect([1, 2, 3]) + ->each() + ->not() + ->toEqual(4); + + expect(static::getCount())->toBe(3); + + expect([1, 2, 3]) + ->each() + ->not->toBeString; + + expect(static::getCount())->toBe(7); +}); + +test('chained opposite and non-opposite expectations', function () { + expect([1, 2, 3]) + ->each() + ->not() + ->toEqual(4) + ->toBeInt(); + + expect(static::getCount())->toBe(6); +}); + +it('can add expectations via "and"', function () { + expect([1, 2, 3]) + ->each() + ->toBeInt // + 3 + ->and([4, 5, 6]) + ->each + ->toBeLessThan(7) // + 3 + ->not + ->toBeLessThan(3) + ->toBeGreaterThan(3) // + 3 + ->and('Hello World') + ->toBeString // + 1 + ->toEqual('Hello World'); // + 1 + + expect(static::getCount())->toBe(14); +}); + +it('accepts callables', function () { + expect([1, 2, 3])->each(function ($number) { + expect($number)->toBeInstanceOf(Expectation::class); + expect($number->value)->toBeInt(); + $number->toBeInt->not->toBeString; + }); + + expect(static::getCount())->toBe(12); +}); diff --git a/tests/Features/Expect/extend.php b/tests/Features/Expect/extend.php new file mode 100644 index 00000000..4be88241 --- /dev/null +++ b/tests/Features/Expect/extend.php @@ -0,0 +1,29 @@ +extend('toBeAMacroExpectation', function () { + $this->toBeTrue(); + + return $this; +}); + +expect()->extend('toBeAMacroExpectationWithArguments', function (bool $value) { + $this->toBe($value); + + return $this; +}); + +it('macros true is true', function () { + expect(true)->toBeAMacroExpectation(); +}); + +it('macros false is not true', function () { + expect(false)->not->toBeAMacroExpectation(); +}); + +it('macros true is true with argument', function () { + expect(true)->toBeAMacroExpectationWithArguments(true); +}); + +it('macros false is not true with argument', function () { + expect(false)->not->toBeAMacroExpectationWithArguments(true); +}); diff --git a/tests/Features/Expect/not.php b/tests/Features/Expect/not.php new file mode 100644 index 00000000..79220b19 --- /dev/null +++ b/tests/Features/Expect/not.php @@ -0,0 +1,10 @@ +toBeTrue() + ->not()->toBeFalse() + ->not->toBeFalse + ->and(false) + ->toBeFalse(); +}); diff --git a/tests/Features/Expect/ray.php b/tests/Features/Expect/ray.php new file mode 100644 index 00000000..9831a1f7 --- /dev/null +++ b/tests/Features/Expect/ray.php @@ -0,0 +1,5 @@ +ray()->toBe(true); +}); diff --git a/tests/Features/Expect/sequence.php b/tests/Features/Expect/sequence.php new file mode 100644 index 00000000..fe6cb635 --- /dev/null +++ b/tests/Features/Expect/sequence.php @@ -0,0 +1,46 @@ +each->sequence(); +})->throws(BadMethodCallException::class, 'Expectation value is not iterable.'); + +test('allows for sequences of checks to be run on iterable data', function () { + expect([1, 2, 3]) + ->sequence( + function ($expectation) { $expectation->toBeInt()->toEqual(1); }, + function ($expectation) { $expectation->toBeInt()->toEqual(2); }, + function ($expectation) { $expectation->toBeInt()->toEqual(3); }, + ); + + expect(static::getCount())->toBe(6); +}); + +test('loops back to the start if it runs out of sequence items', function () { + expect([1, 2, 3, 1, 2, 3, 1, 2]) + ->sequence( + function ($expectation) { $expectation->toBeInt()->toEqual(1); }, + function ($expectation) { $expectation->toBeInt()->toEqual(2); }, + function ($expectation) { $expectation->toBeInt()->toEqual(3); }, + ); + + expect(static::getCount())->toBe(16); +}); + +test('it works if the number of items in the iterable is smaller than the number of expectations', function () { + expect([1, 2]) + ->sequence( + function ($expectation) { $expectation->toBeInt()->toEqual(1); }, + function ($expectation) { $expectation->toBeInt()->toEqual(2); }, + function ($expectation) { $expectation->toBeInt()->toEqual(3); }, + ); + + expect(static::getCount())->toBe(4); +}); + +test('it works with associative arrays', function () { + expect(['foo' => 'bar', 'baz' => 'boom']) + ->sequence( + function ($expectation, $key) { $expectation->toEqual('bar'); $key->toEqual('foo'); }, + function ($expectation, $key) { $expectation->toEqual('boom'); $key->toEqual('baz'); }, + ); +}); diff --git a/tests/Features/Expect/toBe.php b/tests/Features/Expect/toBe.php new file mode 100644 index 00000000..ef2bce5a --- /dev/null +++ b/tests/Features/Expect/toBe.php @@ -0,0 +1,20 @@ +toBeTrue()->and(false)->toBeFalse(); + +test('strict comparisons', function () { + $nuno = new stdClass(); + $dries = new stdClass(); + + expect($nuno)->toBe($nuno)->not->toBe($dries); +}); + +test('failures', function () { + expect(1)->toBe(2); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(1)->not->toBe(1); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeArray.php b/tests/Features/Expect/toBeArray.php new file mode 100644 index 00000000..3fcec231 --- /dev/null +++ b/tests/Features/Expect/toBeArray.php @@ -0,0 +1,16 @@ +toBeArray(); + expect('1, 2, 3')->not->toBeArray(); +}); + +test('failures', function () { + expect(null)->toBeArray(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(['a', 'b', 'c'])->not->toBeArray(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeBool.php b/tests/Features/Expect/toBeBool.php new file mode 100644 index 00000000..5dc37b21 --- /dev/null +++ b/tests/Features/Expect/toBeBool.php @@ -0,0 +1,16 @@ +toBeBool(); + expect(0)->not->toBeBool(); +}); + +test('failures', function () { + expect(null)->toBeBool(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(false)->not->toBeBool(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeCallable.php b/tests/Features/Expect/toBeCallable.php new file mode 100644 index 00000000..2bd8f139 --- /dev/null +++ b/tests/Features/Expect/toBeCallable.php @@ -0,0 +1,18 @@ +toBeCallable(); + expect(null)->not->toBeCallable(); +}); + +test('failures', function () { + $hello = 5; + + expect($hello)->toBeCallable(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(function () { return 42; })->not->toBeCallable(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeDirectory.php b/tests/Features/Expect/toBeDirectory.php new file mode 100644 index 00000000..f30df144 --- /dev/null +++ b/tests/Features/Expect/toBeDirectory.php @@ -0,0 +1,17 @@ +toBeDirectory(); +}); + +test('failures', function () { + expect('/random/path/whatever')->toBeDirectory(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect('.')->not->toBeDirectory(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeEmpty.php b/tests/Features/Expect/toBeEmpty.php new file mode 100644 index 00000000..05e5180a --- /dev/null +++ b/tests/Features/Expect/toBeEmpty.php @@ -0,0 +1,18 @@ +toBeEmpty(); + expect(null)->toBeEmpty(); +}); + +test('failures', function () { + expect([1, 2])->toBeEmpty(); + expect(' ')->toBeEmpty(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect([])->not->toBeEmpty(); + expect(null)->not->toBeEmpty(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeFalse.php b/tests/Features/Expect/toBeFalse.php new file mode 100644 index 00000000..1371c86e --- /dev/null +++ b/tests/Features/Expect/toBeFalse.php @@ -0,0 +1,15 @@ +toBeFalse(); +}); + +test('failures', function () { + expect('')->toBeFalse(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(false)->not->toBe(false); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeFile.php b/tests/Features/Expect/toBeFile.php new file mode 100644 index 00000000..e8bc59d1 --- /dev/null +++ b/tests/Features/Expect/toBeFile.php @@ -0,0 +1,23 @@ +tempFile = sys_get_temp_dir() . '/fake.file'); +}); + +afterEach(function () { + unlink($this->tempFile); +}); + +test('pass', function () { + expect($this->tempFile)->toBeFile(); +}); + +test('failures', function () { + expect('/random/path/whatever.file')->toBeFile(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect($this->tempFile)->not->toBeFile(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeFloat.php b/tests/Features/Expect/toBeFloat.php new file mode 100644 index 00000000..69aa2306 --- /dev/null +++ b/tests/Features/Expect/toBeFloat.php @@ -0,0 +1,16 @@ +toBeFloat(); + expect(1)->not->toBeFloat(); +}); + +test('failures', function () { + expect(42)->toBeFloat(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(log(3))->not->toBeFloat(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeGreatherThan.php b/tests/Features/Expect/toBeGreatherThan.php new file mode 100644 index 00000000..ed840ed6 --- /dev/null +++ b/tests/Features/Expect/toBeGreatherThan.php @@ -0,0 +1,16 @@ +toBeGreaterThan(41); + expect(4)->toBeGreaterThan(3.9); +}); + +test('failures', function () { + expect(4)->toBeGreaterThan(4); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(5)->not->toBeGreaterThan(4); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeGreatherThanOrEqual.php b/tests/Features/Expect/toBeGreatherThanOrEqual.php new file mode 100644 index 00000000..3b1cd029 --- /dev/null +++ b/tests/Features/Expect/toBeGreatherThanOrEqual.php @@ -0,0 +1,16 @@ +toBeGreaterThanOrEqual(41); + expect(4)->toBeGreaterThanOrEqual(4); +}); + +test('failures', function () { + expect(4)->toBeGreaterThanOrEqual(4.1); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(5)->not->toBeGreaterThanOrEqual(5); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeInfinite.php b/tests/Features/Expect/toBeInfinite.php new file mode 100644 index 00000000..ebcb0a36 --- /dev/null +++ b/tests/Features/Expect/toBeInfinite.php @@ -0,0 +1,16 @@ +toBeInfinite(); + expect(log(1))->not->toBeInfinite(); +}); + +test('failures', function () { + expect(asin(2))->toBeInfinite(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(INF)->not->toBeInfinite(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeInstanceOf.php b/tests/Features/Expect/toBeInstanceOf.php new file mode 100644 index 00000000..03b3e881 --- /dev/null +++ b/tests/Features/Expect/toBeInstanceOf.php @@ -0,0 +1,16 @@ +toBeInstanceOf(Exception::class); + expect(new Exception())->not->toBeInstanceOf(RuntimeException::class); +}); + +test('failures', function () { + expect(new Exception())->toBeInstanceOf(RuntimeException::class); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(new Exception())->not->toBeInstanceOf(Exception::class); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeInt.php b/tests/Features/Expect/toBeInt.php new file mode 100644 index 00000000..9eb1b3db --- /dev/null +++ b/tests/Features/Expect/toBeInt.php @@ -0,0 +1,16 @@ +toBeInt(); + expect(42.0)->not->toBeInt(); +}); + +test('failures', function () { + expect(42.0)->toBeInt(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(6 * 7)->not->toBeInt(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeIterable.php b/tests/Features/Expect/toBeIterable.php new file mode 100644 index 00000000..fca23688 --- /dev/null +++ b/tests/Features/Expect/toBeIterable.php @@ -0,0 +1,23 @@ +toBeIterable(); + expect(null)->not->toBeIterable(); +}); + +test('failures', function () { + expect(42)->toBeIterable(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + function gen(): iterable + { + yield 1; + yield 2; + yield 3; + } + + expect(gen())->not->toBeIterable(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeJson.php b/tests/Features/Expect/toBeJson.php new file mode 100644 index 00000000..7ef8bc78 --- /dev/null +++ b/tests/Features/Expect/toBeJson.php @@ -0,0 +1,17 @@ +toBeJson(); + expect('foo')->not->toBeJson(); + expect('{"hello"')->not->toBeJson(); +}); + +test('failures', function () { + expect(':"world"}')->toBeJson(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect('{"hello":"world"}')->not->toBeJson(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeLessThan.php b/tests/Features/Expect/toBeLessThan.php new file mode 100644 index 00000000..08db8545 --- /dev/null +++ b/tests/Features/Expect/toBeLessThan.php @@ -0,0 +1,16 @@ +toBeLessThan(42); + expect(4)->toBeLessThan(5); +}); + +test('failures', function () { + expect(4)->toBeLessThan(4); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(5)->not->toBeLessThan(6); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeLessThanOrEqual.php b/tests/Features/Expect/toBeLessThanOrEqual.php new file mode 100644 index 00000000..c9f00577 --- /dev/null +++ b/tests/Features/Expect/toBeLessThanOrEqual.php @@ -0,0 +1,16 @@ +toBeLessThanOrEqual(42); + expect(4)->toBeLessThanOrEqual(4); +}); + +test('failures', function () { + expect(4)->toBeLessThanOrEqual(3.9); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(5)->not->toBeLessThanOrEqual(5); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeNAN.php b/tests/Features/Expect/toBeNAN.php new file mode 100644 index 00000000..0767f1ad --- /dev/null +++ b/tests/Features/Expect/toBeNAN.php @@ -0,0 +1,16 @@ +toBeNan(); + expect(log(0))->not->toBeNan(); +}); + +test('failures', function () { + expect(1)->toBeNan(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(acos(1.5))->not->toBeNan(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeNull.php b/tests/Features/Expect/toBeNull.php new file mode 100644 index 00000000..2bd7d987 --- /dev/null +++ b/tests/Features/Expect/toBeNull.php @@ -0,0 +1,16 @@ +toBeNull(); + expect('')->not->toBeNull(); +}); + +test('failures', function () { + expect('hello')->toBeNull(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(null)->not->toBeNull(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeNumeric.php b/tests/Features/Expect/toBeNumeric.php new file mode 100644 index 00000000..710ca236 --- /dev/null +++ b/tests/Features/Expect/toBeNumeric.php @@ -0,0 +1,16 @@ +toBeNumeric(); + expect('A')->not->toBeNumeric(); +}); + +test('failures', function () { + expect(null)->toBeNumeric(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(6 * 7)->not->toBeNumeric(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeObject.php b/tests/Features/Expect/toBeObject.php new file mode 100644 index 00000000..e227b4cb --- /dev/null +++ b/tests/Features/Expect/toBeObject.php @@ -0,0 +1,16 @@ + 1])->toBeObject(); + expect(['a' => 1])->not->toBeObject(); +}); + +test('failures', function () { + expect(null)->toBeObject(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect((object) 'ciao')->not->toBeObject(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeReadableDirectory.php b/tests/Features/Expect/toBeReadableDirectory.php new file mode 100644 index 00000000..704109b5 --- /dev/null +++ b/tests/Features/Expect/toBeReadableDirectory.php @@ -0,0 +1,15 @@ +toBeReadableDirectory(); +}); + +test('failures', function () { + expect('/random/path/whatever')->toBeReadableDirectory(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(sys_get_temp_dir())->not->toBeReadableDirectory(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeReadableFile.php b/tests/Features/Expect/toBeReadableFile.php new file mode 100644 index 00000000..756046fa --- /dev/null +++ b/tests/Features/Expect/toBeReadableFile.php @@ -0,0 +1,23 @@ +tempFile = sys_get_temp_dir() . '/fake.file'); +}); + +afterEach(function () { + unlink($this->tempFile); +}); + +test('pass', function () { + expect($this->tempFile)->toBeReadableFile(); +}); + +test('failures', function () { + expect('/random/path/whatever.file')->toBeReadableFile(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect($this->tempFile)->not->toBeReadableFile(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeResource.php b/tests/Features/Expect/toBeResource.php new file mode 100644 index 00000000..575fabe3 --- /dev/null +++ b/tests/Features/Expect/toBeResource.php @@ -0,0 +1,22 @@ +toBeResource(); + expect(null)->not->toBeResource(); +}); + +test('failures', function () { + expect(null)->toBeResource(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () use ($resource) { + expect($resource)->not->toBeResource(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeScalar.php b/tests/Features/Expect/toBeScalar.php new file mode 100644 index 00000000..236c0e0b --- /dev/null +++ b/tests/Features/Expect/toBeScalar.php @@ -0,0 +1,15 @@ +toBeScalar(); +}); + +test('failures', function () { + expect(null)->toBeScalar(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(42)->not->toBeScalar(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeString.php b/tests/Features/Expect/toBeString.php new file mode 100644 index 00000000..91c31881 --- /dev/null +++ b/tests/Features/Expect/toBeString.php @@ -0,0 +1,16 @@ +toBeString(); + expect(1.1)->not->toBeString(); +}); + +test('failures', function () { + expect(null)->toBeString(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect('42')->not->toBeString(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeTrue.php b/tests/Features/Expect/toBeTrue.php new file mode 100644 index 00000000..8e998f9b --- /dev/null +++ b/tests/Features/Expect/toBeTrue.php @@ -0,0 +1,15 @@ +toBeTrue(); +}); + +test('failures', function () { + expect('')->toBeTrue(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(false)->not->toBe(false); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeWritableDirectory.php b/tests/Features/Expect/toBeWritableDirectory.php new file mode 100644 index 00000000..88f96019 --- /dev/null +++ b/tests/Features/Expect/toBeWritableDirectory.php @@ -0,0 +1,15 @@ +toBeWritableDirectory(); +}); + +test('failures', function () { + expect('/random/path/whatever')->toBeWritableDirectory(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(sys_get_temp_dir())->not->toBeWritableDirectory(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toBeWritableFile.php b/tests/Features/Expect/toBeWritableFile.php new file mode 100644 index 00000000..96fe05c3 --- /dev/null +++ b/tests/Features/Expect/toBeWritableFile.php @@ -0,0 +1,23 @@ +tempFile = sys_get_temp_dir() . '/fake.file'); +}); + +afterEach(function () { + unlink($this->tempFile); +}); + +test('pass', function () { + expect($this->tempFile)->toBeWritableFile(); +}); + +test('failures', function () { + expect('/random/path/whatever.file')->toBeWritableFile(); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect($this->tempFile)->not->toBeWritableFile(); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toContain.php b/tests/Features/Expect/toContain.php new file mode 100644 index 00000000..04d586ff --- /dev/null +++ b/tests/Features/Expect/toContain.php @@ -0,0 +1,19 @@ +toContain(42); +}); + +test('passes arrays', function () { + expect('Nuno')->toContain('Nu'); +}); + +test('failures', function () { + expect([1, 2, 42])->toContain(3); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect([1, 2, 42])->not->toContain(42); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toEndWith.php b/tests/Features/Expect/toEndWith.php new file mode 100644 index 00000000..e669795e --- /dev/null +++ b/tests/Features/Expect/toEndWith.php @@ -0,0 +1,15 @@ +toEndWith('name'); +}); + +test('failures', function () { + expect('username')->toEndWith('password'); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect('username')->not->toEndWith('name'); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toEqual.php b/tests/Features/Expect/toEqual.php new file mode 100644 index 00000000..de0b7e50 --- /dev/null +++ b/tests/Features/Expect/toEqual.php @@ -0,0 +1,15 @@ +toEqual(123); +}); + +test('failures', function () { + expect(['a', 'b', 'c'])->toEqual(['a', 'b']); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect('042')->not->toEqual(42); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toEqualCanonicalizing.php b/tests/Features/Expect/toEqualCanonicalizing.php new file mode 100644 index 00000000..5e1178b9 --- /dev/null +++ b/tests/Features/Expect/toEqualCanonicalizing.php @@ -0,0 +1,16 @@ +toEqualCanonicalizing([3, 1, 2]); + expect(['g', 'a', 'z'])->not->toEqualCanonicalizing(['a', 'z']); +}); + +test('failures', function () { + expect([3, 2, 1])->toEqualCanonicalizing([1, 2]); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(['a', 'b', 'c'])->not->toEqualCanonicalizing(['b', 'a', 'c']); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toEqualWithDelta.php b/tests/Features/Expect/toEqualWithDelta.php new file mode 100644 index 00000000..bb78b86d --- /dev/null +++ b/tests/Features/Expect/toEqualWithDelta.php @@ -0,0 +1,15 @@ +toEqualWithDelta(1.3, .4); +}); + +test('failures', function () { + expect(1.0)->toEqualWithDelta(1.5, .1); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(1.0)->not->toEqualWithDelta(1.6, .7); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toHaveCount.php b/tests/Features/Expect/toHaveCount.php new file mode 100644 index 00000000..a1948260 --- /dev/null +++ b/tests/Features/Expect/toHaveCount.php @@ -0,0 +1,15 @@ +toHaveCount(3); +}); + +test('failures', function () { + expect([1, 2, 3])->toHaveCount(4); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect([1, 2, 3])->not->toHaveCount(3); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toHaveKey.php b/tests/Features/Expect/toHaveKey.php new file mode 100644 index 00000000..1695688b --- /dev/null +++ b/tests/Features/Expect/toHaveKey.php @@ -0,0 +1,15 @@ + 1, 'b', 'c' => 'world'])->toHaveKey('c'); +}); + +test('failures', function () { + expect(['a' => 1, 'b', 'c' => 'world'])->toHaveKey('hello'); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(['a' => 1, 'hello' => 'world', 'c'])->not->toHaveKey('hello'); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toHaveKeys.php b/tests/Features/Expect/toHaveKeys.php new file mode 100644 index 00000000..a09c8809 --- /dev/null +++ b/tests/Features/Expect/toHaveKeys.php @@ -0,0 +1,15 @@ + 1, 'b', 'c' => 'world'])->toHaveKeys(['a', 'c']); +}); + +test('failures', function () { + expect(['a' => 1, 'b', 'c' => 'world'])->toHaveKeys(['a', 'd']); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(['a' => 1, 'hello' => 'world', 'c'])->not->toHaveKeys(['hello', 'c']); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toHaveProperty.php b/tests/Features/Expect/toHaveProperty.php new file mode 100644 index 00000000..19b4c92e --- /dev/null +++ b/tests/Features/Expect/toHaveProperty.php @@ -0,0 +1,22 @@ +foo = 'bar'; +$obj->fooNull = null; + +test('pass', function () use ($obj) { + expect($obj)->toHaveProperty('foo'); + expect($obj)->toHaveProperty('foo', 'bar'); + expect($obj)->toHaveProperty('fooNull'); + expect($obj)->toHaveProperty('fooNull', null); +}); + +test('failures', function () use ($obj) { + expect($obj)->toHaveProperty('bar'); +})->throws(ExpectationFailedException::class); + +test('not failures', function () use ($obj) { + expect($obj)->not->toHaveProperty('foo'); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toMatch.php b/tests/Features/Expect/toMatch.php new file mode 100644 index 00000000..9088a3c4 --- /dev/null +++ b/tests/Features/Expect/toMatch.php @@ -0,0 +1,15 @@ +toMatch('/^hello wo.*$/i'); +}); + +test('failures', function () { + expect('Hello World')->toMatch('/^hello$/i'); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect('Hello World')->not->toMatch('/^hello wo.*$/i'); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toMatchArray.php b/tests/Features/Expect/toMatchArray.php new file mode 100644 index 00000000..b1646b9e --- /dev/null +++ b/tests/Features/Expect/toMatchArray.php @@ -0,0 +1,31 @@ +user = [ + 'id' => 1, + 'name' => 'Nuno', + 'email' => 'enunomaduro@gmail.com', + ]; +}); + +test('pass', function () { + expect($this->user)->toMatchArray([ + 'name' => 'Nuno', + 'email' => 'enunomaduro@gmail.com', + ]); +}); + +test('failures', function () { + expect($this->user)->toMatchArray([ + 'name' => 'Not the same name', + 'email' => 'enunomaduro@gmail.com', + ]); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect($this->user)->not->toMatchArray([ + 'id' => 1, + ]); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toMatchConstraint.php b/tests/Features/Expect/toMatchConstraint.php new file mode 100644 index 00000000..e5b4b681 --- /dev/null +++ b/tests/Features/Expect/toMatchConstraint.php @@ -0,0 +1,16 @@ +toMatchConstraint(new IsTrue()); +}); + +test('failures', function () { + expect(false)->toMatchConstraint(new IsTrue()); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect(true)->not->toMatchConstraint(new IsTrue()); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toMatchObject.php b/tests/Features/Expect/toMatchObject.php new file mode 100644 index 00000000..fd2c358b --- /dev/null +++ b/tests/Features/Expect/toMatchObject.php @@ -0,0 +1,31 @@ +user = (object) [ + 'id' => 1, + 'name' => 'Nuno', + 'email' => 'enunomaduro@gmail.com', + ]; +}); + +test('pass', function () { + expect($this->user)->toMatchObject([ + 'name' => 'Nuno', + 'email' => 'enunomaduro@gmail.com', + ]); +}); + +test('failures', function () { + expect($this->user)->toMatchObject([ + 'name' => 'Not the same name', + 'email' => 'enunomaduro@gmail.com', + ]); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect($this->user)->not->toMatchObject([ + 'id' => 1, + ]); +})->throws(ExpectationFailedException::class); diff --git a/tests/Features/Expect/toStartWith.php b/tests/Features/Expect/toStartWith.php new file mode 100644 index 00000000..64465869 --- /dev/null +++ b/tests/Features/Expect/toStartWith.php @@ -0,0 +1,15 @@ +toStartWith('user'); +}); + +test('failures', function () { + expect('username')->toStartWith('password'); +})->throws(ExpectationFailedException::class); + +test('not failures', function () { + expect('username')->not->toStartWith('user'); +})->throws(ExpectationFailedException::class);