From b2250c65a0373a17b53a98f8c9a38a1b0e293f53 Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Sun, 17 Mar 2024 22:24:13 +0100 Subject: [PATCH] Improve how we handle data providers We frequently repeat ourselves in the data providers. There's a clear advantage to that, as it increases the number of possibilities with the test, but it can also be annoying when we have to repeat ourselves. Signed-off-by: Henrique Moody --- tests/fixtures/data-provider.php | 167 ++++++++++++++++ tests/library/DataProvider.php | 64 ++++++ .../DataProvider/UndefinedProvider.php | 40 ---- tests/library/TestCase.php | 187 ++++++------------ .../unit/Helpers/CanValidateUndefinedTest.php | 2 - tests/unit/Rules/ResourceTypeTest.php | 42 ++-- 6 files changed, 299 insertions(+), 203 deletions(-) create mode 100644 tests/fixtures/data-provider.php create mode 100644 tests/library/DataProvider.php delete mode 100644 tests/library/DataProvider/UndefinedProvider.php diff --git a/tests/fixtures/data-provider.php b/tests/fixtures/data-provider.php new file mode 100644 index 00000000..607a8de1 --- /dev/null +++ b/tests/fixtures/data-provider.php @@ -0,0 +1,167 @@ + + * SPDX-License-Identifier: MIT + */ + +declare(strict_types=1); + +use Respect\Validation\Test\Stubs\WithProperties; +use Respect\Validation\Test\Stubs\WithStaticProperties; +use Respect\Validation\Test\Stubs\WithUninitialized; + +return [ + 'null' => [ + 'value' => [null], + 'tags' => ['nullType', 'empty', 'undefined'], + ], + + // BooleanTypes + 'false' => [ + 'value' => [false], + 'tags' => ['boolType', 'false', 'empty'], + ], + 'true' => [ + 'value' => [true], + 'tags' => ['boolType', 'true'], + ], + + // IntegerTypes + 'zero integer' => [ + 'value' => [0], + 'tags' => ['intType', 'zero'], + ], + 'positive integer' => [ + 'value' => [PHP_INT_MAX], + 'tags' => ['intType', 'positive'], + ], + 'negative integer' => [ + 'value' => [PHP_INT_MIN], + 'tags' => ['intType', 'negative'], + ], + + // StringTypes + 'string' => [ + 'value' => ['string'], + 'tags' => ['stringType'], + ], + 'empty string' => [ + 'value' => [''], + 'tags' => ['stringType', 'empty', 'undefined'], + ], + 'integer string' => [ + 'value' => ['500'], + 'tags' => ['stringType', 'intVal', 'positive'], + ], + 'float string' => [ + 'value' => ['56.8'], + 'tags' => ['stringType', 'floatVal', 'positive'], + ], + 'zero string' => [ + 'value' => ['0'], + 'tags' => ['stringType', 'intVal', 'zero'], + ], + + // Float types + 'zero float' => [ + 'value' => [0.0], + 'tags' => ['floatType', 'zero'], + ], + 'positive float' => [ + 'value' => [32.890], + 'tags' => ['floatType', 'positive'], + ], + 'negative float' => [ + 'value' => [-893.1], + 'tags' => ['floatType', 'negative'], + ], + + // Array types + 'array list' => [ + 'value' => [[4, 5, 6]], + 'tags' => ['arrayType', 'iterableType', 'countable'], + ], + 'array associative with string keys' => [ + 'value' => [['broccoli' => 89, 'spinach' => 123, 'beets' => 90]], + 'tags' => ['arrayType', 'iterableType', 'countable'], + ], + 'array associative with int keys' => [ + 'value' => [[1 => 'cauliflower', 2 => 'eggplant', 3 => 'asparagus']], + 'tags' => ['arrayType', 'iterableType', 'countable'], + ], + 'empty array' => [ + 'value' => [[]], + 'tags' => ['arrayType', 'iterableType', 'countable', 'empty'], + ], + + // Array values + 'ArrayObject' => [ + 'value' => [new ArrayObject([1, 2, 3])], + 'tags' => ['objectType', 'iterableType', 'ArrayObject', 'countable'], + ], + 'empty ArrayObject' => [ + 'value' => [new ArrayObject([])], + 'tags' => ['objectType', 'iterableType', 'ArrayObject', 'countable', 'empty'], + ], + + // Iterable types + 'generator' => [ + 'value' => [(static fn() => yield 7)()], // phpcs:ignore + 'tags' => ['objectType', 'iterableType', 'generator'], + ], + 'empty generator' => [ + 'value' => [(static fn() => yield from [])()], + 'tags' => ['objectType', 'iterableType', 'generator', 'empty'], + ], + + // Callable types + 'closure' => [ + 'value' => [static fn() => 'foo'], + 'tags' => ['objectType', 'callable'], + ], + + // Object types + 'object' => [ + 'value' => [new stdClass()], + 'tags' => ['objectType'], + ], + 'object with properties' => [ + 'value' => [new WithProperties()], + 'tags' => ['objectType'], + ], + 'object with uninitialized properties' => [ + 'value' => [new WithUninitialized()], + 'tags' => ['objectType'], + ], + 'object with static properties' => [ + 'value' => [new WithStaticProperties()], + 'tags' => ['objectType'], + ], + + // Resource types + 'stream-context resource' => [ + 'value' => [stream_context_create()], + 'tags' => ['resourceType'], + ], + 'stream resource' => [ + 'value' => [tmpfile()], + 'tags' => ['resourceType'], + ], + + // Closed resource (is not a resourceType) + 'resource (closed)' => [ + 'value' => [ + (static function () { + $resource = tmpfile(); + if ($resource === false) { + throw new RuntimeException('Failed to create temporary file.'); + } + fclose($resource); + + return $resource; + })(), + ], + 'tags' => ['closedResource'], + ], +]; diff --git a/tests/library/DataProvider.php b/tests/library/DataProvider.php new file mode 100644 index 00000000..3bde715b --- /dev/null +++ b/tests/library/DataProvider.php @@ -0,0 +1,64 @@ + + * SPDX-License-Identifier: MIT + */ + +declare(strict_types=1); + +namespace Respect\Validation\Test; + +use ArrayIterator; +use IteratorAggregate; +use Traversable; + +use function array_filter; +use function array_intersect; +use function array_map; + +/** + * @implements IteratorAggregate> + */ +final class DataProvider implements IteratorAggregate +{ + /** + * @param array $data + */ + public function __construct( + private readonly array $data + ) { + } + + public function with(string ...$tags): self + { + return new self(array_filter( + $this->data, + static fn($value) => array_intersect($tags, $value['tags']) === $tags + )); + } + + public function withAny(string ...$tags): self + { + return new self(array_filter( + $this->data, + static fn($value) => array_intersect($tags, $value['tags']) !== [] + )); + } + + public function without(string ...$tags): self + { + return new self(array_filter( + $this->data, + static fn($value) => array_intersect($tags, $value['tags']) === [] + )); + } + + /** + * @return Traversable> + */ + public function getIterator(): Traversable + { + return new ArrayIterator(array_map(static fn(array $value) => $value['value'], $this->data)); + } +} diff --git a/tests/library/DataProvider/UndefinedProvider.php b/tests/library/DataProvider/UndefinedProvider.php deleted file mode 100644 index f23bce80..00000000 --- a/tests/library/DataProvider/UndefinedProvider.php +++ /dev/null @@ -1,40 +0,0 @@ - - * SPDX-License-Identifier: MIT - */ - -declare(strict_types=1); - -namespace Respect\Validation\Test\DataProvider; - -trait UndefinedProvider -{ - /** - * @return mixed[][] - */ - public static function providerForUndefined(): array - { - return [ - [null], - [''], - ]; - } - - /** - * @return mixed[][] - */ - public static function providerForNotUndefined(): array - { - return [ - [0], - [0.0], - ['0'], - [false], - [true], - [' '], - [[]], - ]; - } -} diff --git a/tests/library/TestCase.php b/tests/library/TestCase.php index 31456dda..e803bb41 100644 --- a/tests/library/TestCase.php +++ b/tests/library/TestCase.php @@ -9,7 +9,6 @@ declare(strict_types=1); namespace Respect\Validation\Test; -use ArrayObject; use PHPUnit\Framework\TestCase as PHPUnitTestCase; use Respect\Validation\Test\Stubs\WithProperties; use Respect\Validation\Test\Stubs\WithStaticProperties; @@ -17,7 +16,6 @@ use Respect\Validation\Test\Stubs\WithUninitialized; use Respect\Validation\Validatable; use stdClass; -use function array_merge; use function implode; use function ltrim; use function realpath; @@ -25,10 +23,6 @@ use function Respect\Stringifier\stringify; use function sprintf; use function strrchr; use function substr; -use function tmpfile; - -use const PHP_INT_MAX; -use const PHP_INT_MIN; abstract class TestCase extends PHPUnitTestCase { @@ -72,178 +66,109 @@ abstract class TestCase extends PHPUnitTestCase ); } - /** @return array */ - public static function providerForAnyValues(): array + public static function providerForAnyValues(): DataProvider { - return array_merge( - self::providerForStringTypes(), - self::providerForNonScalarValues(), - self::providerForEmptyIterableValues(), - self::providerForNonEmptyIterableValues(), - self::providerForNonIterableTypes(), - self::providerForIntegerTypes(), - self::providerForBooleanTypes(), - self::providerForFloatTypes(), - ); + return new DataProvider((include __DIR__ . '/../fixtures/data-provider.php')); } - /** @return array */ - public static function providerForScalarValues(): array + public static function providerForScalarValues(): DataProvider { - return array_merge( - self::providerForStringTypes(), - self::providerForIntegerTypes(), - self::providerForBooleanTypes(), - self::providerForFloatTypes(), - ); + return self::providerForAnyValues()->withAny('stringType', 'boolType', 'intType', 'floatType'); } - /** @return array */ - public static function providerForEmptyScalarValues(): array + public static function providerForEmptyScalarValues(): DataProvider { - return [ - 'empty string' => [''], - 'false' => [false], - ]; + return self::providerForAnyValues()->withAny('stringType', 'boolType')->with('empty'); } - /** @return array */ - public static function providerForNonScalarValues(): array + public static function providerForNonScalarValues(): DataProvider { - return self::providerForNonEmptyIterableValues() + self::providerForNonEmptyIterableValues() + [ - 'closure' => [static fn() => 'foo'], - 'stdClass' => [new stdClass()], - 'null' => [null], - 'resource' => [tmpfile()], - ]; + return self::providerForAnyValues()->without('stringType', 'boolType', 'intType', 'floatType'); } - /** @return array */ - public static function providerForNonIterableTypes(): array + public static function providerForNonIterableTypes(): DataProvider { - return array_merge( - self::providerForScalarValues(), - [ - 'closure' => [static fn() => 'foo'], - 'stdClass' => [new stdClass()], - 'null' => [null], - 'resource' => [tmpfile()], - ] - ); + return self::providerForAnyValues()->without('iterableType'); } - /** @return array}> */ - public static function providerForIterableTypes(): array + public static function providerForIterableTypes(): DataProvider { - return array_merge( - self::providerForNonEmptyIterableValues(), - self::providerForEmptyIterableValues(), - ); + return self::providerForAnyValues()->with('iterableType'); } - /** @return array}> */ - public static function providerForNonEmptyIterableValues(): array + public static function providerForNonEmptyIterableValues(): DataProvider { - return [ - 'ArrayObject' => [new ArrayObject([1, 2, 3])], - 'array' => [[4, 5, 6]], - 'generator' => [(static fn() => yield 7)()], // phpcs:ignore - ]; + return self::providerForAnyValues()->with('iterableType')->without('empty'); } - /** @return array}> */ - public static function providerForEmptyIterableValues(): array + public static function providerForEmptyIterableValues(): DataProvider { - return [ - 'empty ArrayObject' => [new ArrayObject([])], - 'empty array' => [[]], - 'empty generator' => [(static fn() => yield from [])()], - ]; + return self::providerForAnyValues()->with('iterableType', 'empty'); } - /** @return array */ - public static function providerForStringTypes(): array + public static function providerForStringTypes(): DataProvider { - return [ - 'string' => ['string'], - 'empty string' => [''], - 'integer string' => ['500'], - 'float string' => ['56.8'], - 'zero string' => ['0'], - ]; + return self::providerForAnyValues()->with('stringType'); } - /** @return array */ - public static function providerForNonEmptyStringTypes(): array + public static function providerForCountable(): DataProvider { - $dataProvider = self::providerForStringTypes(); - unset($dataProvider['empty string']); - - return $dataProvider; + return self::providerForAnyValues()->with('countable'); } - /** @return array */ - public static function providerForNonStringTypes(): array + public static function providerForNonEmptyStringTypes(): DataProvider { - return array_merge( - self::providerForNonScalarValues(), - self::providerForIntegerTypes(), - self::providerForBooleanTypes(), - self::providerForFloatTypes(), - ); + return self::providerForAnyValues()->with('stringType')->without('empty'); } - /** @return array */ - public static function providerForIntegerTypes(): array + public static function providerForNonStringTypes(): DataProvider { - return [ - 'zero integer' => [0], - 'positive integer' => [PHP_INT_MAX], - 'negative integer' => [PHP_INT_MIN], - ]; + return self::providerForAnyValues()->without('stringType'); } - /** @return array */ - public static function providerForBooleanTypes(): array + public static function providerForIntegerTypes(): DataProvider { - return [ - 'true' => [true], - 'false' => [false], - ]; + return self::providerForAnyValues()->with('intType'); } - /** @return array */ - public static function providerForFloatTypes(): array + public static function providerForBooleanTypes(): DataProvider { - return [ - 'zero float' => [0.0], - 'negative float' => [-893.1], - 'positive float' => [32.890], - ]; + return self::providerForAnyValues()->with('boolType'); } - /** @return array */ - public static function providerForNonArrayTypes(): array + public static function providerForFloatTypes(): DataProvider { - $scalarValues = self::providerForNonScalarValues(); - unset($scalarValues['array']); - - return array_merge( - self::providerForIntegerTypes(), - self::providerForBooleanTypes(), - self::providerForFloatTypes(), - self::providerForStringTypes(), - $scalarValues, - ); + return self::providerForAnyValues()->with('floatType'); } - /** @return array */ - public static function providerForNonArrayValues(): array + public static function providerForNonArrayTypes(): DataProvider { - $arrayTypes = self::providerForNonArrayTypes(); - unset($arrayTypes['ArrayObject']); + return self::providerForAnyValues()->without('arrayType'); + } - return $arrayTypes; + public static function providerForNonArrayValues(): DataProvider + { + return self::providerForAnyValues()->without('arrayType', 'ArrayObject'); + } + + public static function providerForUndefined(): DataProvider + { + return self::providerForAnyValues()->with('undefined'); + } + + public static function providerForNotUndefined(): DataProvider + { + return self::providerForAnyValues()->without('undefined'); + } + + public static function providerForResourceType(): DataProvider + { + return self::providerForAnyValues()->with('resourceType'); + } + + public static function providerForNonResourceType(): DataProvider + { + return self::providerForAnyValues()->without('resourceType'); } /** @return array}> */ diff --git a/tests/unit/Helpers/CanValidateUndefinedTest.php b/tests/unit/Helpers/CanValidateUndefinedTest.php index 76bfee95..53b87ee2 100644 --- a/tests/unit/Helpers/CanValidateUndefinedTest.php +++ b/tests/unit/Helpers/CanValidateUndefinedTest.php @@ -13,7 +13,6 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; -use Respect\Validation\Test\DataProvider\UndefinedProvider; use Respect\Validation\Test\TestCase; #[Group('helper')] @@ -21,7 +20,6 @@ use Respect\Validation\Test\TestCase; final class CanValidateUndefinedTest extends TestCase { use CanValidateUndefined; - use UndefinedProvider; #[Test] #[DataProvider('providerForUndefined')] diff --git a/tests/unit/Rules/ResourceTypeTest.php b/tests/unit/Rules/ResourceTypeTest.php index 152c004b..f92bcd69 100644 --- a/tests/unit/Rules/ResourceTypeTest.php +++ b/tests/unit/Rules/ResourceTypeTest.php @@ -10,44 +10,26 @@ declare(strict_types=1); namespace Respect\Validation\Rules; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; -use Respect\Validation\Test\RuleTestCase; -use stdClass; - -use function stream_context_create; -use function tmpfile; +use PHPUnit\Framework\Attributes\Test; +use Respect\Validation\Test\TestCase; #[Group('rule')] #[CoversClass(ResourceType::class)] -final class ResourceTypeTest extends RuleTestCase +final class ResourceTypeTest extends TestCase { - /** @return iterable */ - public static function providerForValidInput(): iterable + #[Test] + #[DataProvider('providerForResourceType')] + public function shouldValidateValidInput(mixed $input): void { - $rule = new ResourceType(); - - return [ - [$rule, stream_context_create()], - [$rule, tmpfile()], - ]; + self::assertValidInput(new ResourceType(), $input); } - /** @return iterable */ - public static function providerForInvalidInput(): iterable + #[Test] + #[DataProvider('providerForNonResourceType')] + public function shouldValidateInvalidInput(mixed $input): void { - $rule = new ResourceType(); - - return [ - [$rule, 'String'], - [$rule, 123], - [$rule, []], - [ - $rule, - static function (): void { - }, - ], - [$rule, new stdClass()], - [$rule, null], - ]; + self::assertInvalidInput(new ResourceType(), $input); } }