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 <henriquemoody@gmail.com>
This commit is contained in:
Henrique Moody 2024-03-17 22:24:13 +01:00
parent 28b3f88627
commit b2250c65a0
No known key found for this signature in database
GPG key ID: 221E9281655813A6
6 changed files with 299 additions and 203 deletions

167
tests/fixtures/data-provider.php vendored Normal file
View file

@ -0,0 +1,167 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* 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'],
],
];

View file

@ -0,0 +1,64 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* 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<array<array{mixed}>>
*/
final class DataProvider implements IteratorAggregate
{
/**
* @param array<mixed> $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<array<mixed>>
*/
public function getIterator(): Traversable
{
return new ArrayIterator(array_map(static fn(array $value) => $value['value'], $this->data));
}
}

View file

@ -1,40 +0,0 @@
<?php
/*
* Copyright (c) Alexandre Gomes Gaigalas <alganet@gmail.com>
* 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],
[' '],
[[]],
];
}
}

View file

@ -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<array{mixed}> */
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<array{scalar}> */
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<array{scalar}> */
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<array{mixed}> */
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<array{mixed}> */
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<array{iterable<mixed>}> */
public static function providerForIterableTypes(): array
public static function providerForIterableTypes(): DataProvider
{
return array_merge(
self::providerForNonEmptyIterableValues(),
self::providerForEmptyIterableValues(),
);
return self::providerForAnyValues()->with('iterableType');
}
/** @return array<array{iterable<mixed>}> */
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<array{iterable<mixed>}> */
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<array{string}> */
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<array{string}> */
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<array{mixed}> */
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<array{int}> */
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<array{bool}> */
public static function providerForBooleanTypes(): array
public static function providerForIntegerTypes(): DataProvider
{
return [
'true' => [true],
'false' => [false],
];
return self::providerForAnyValues()->with('intType');
}
/** @return array<array{float}> */
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<array{mixed}> */
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<array{mixed}> */
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<string, array{string|int, array<mixed>}> */

View file

@ -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')]

View file

@ -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<array{ResourceType, mixed}> */
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<array{ResourceType, mixed}> */
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);
}
}