Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support faker modifiers on Fabricator #8671

Merged
merged 3 commits into from
Mar 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
77 changes: 76 additions & 1 deletion system/Test/Fabricator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace CodeIgniter\Test;

use Closure;
use CodeIgniter\Exceptions\FrameworkException;
use CodeIgniter\I18n\Time;
use CodeIgniter\Model;
Expand Down Expand Up @@ -88,6 +89,17 @@ class Fabricator
*/
protected $tempOverrides;

/**
* Fields to be modified before applying any formatter.
*
* @var array{
* unique: array<non-empty-string, array{reset: bool, maxRetries: int}>,
* optional: array<non-empty-string, array{weight: float, default: mixed}>,
* valid: array<non-empty-string, array{validator: Closure(mixed): bool|null, maxRetries: int}>
* }
*/
private array $modifiedFields = ['unique' => [], 'optional' => [], 'valid' => []];

/**
* Default formatter to use when nothing is detected
*
Expand Down Expand Up @@ -251,6 +263,46 @@ public function setOverrides(array $overrides = [], $persist = true): self
return $this;
}

/**
* Set a field to be unique.
*
* @param bool $reset If set to true, resets the list of existing values
* @param int $maxRetries Maximum number of retries to find a unique value,
* After which an OverflowException is thrown.
*/
public function setUnique(string $field, bool $reset = false, int $maxRetries = 10000): static
{
$this->modifiedFields['unique'][$field] = compact('reset', 'maxRetries');

return $this;
}

/**
* Set a field to be optional.
*
* @param float $weight A probability between 0 and 1, 0 means that we always get the default value.
*/
public function setOptional(string $field, float $weight = 0.5, mixed $default = null): static
{
$this->modifiedFields['optional'][$field] = compact('weight', 'default');

return $this;
}

/**
* Set a field to be valid using a callback.
*
* @param Closure(mixed): bool|null $validator A function returning true for valid values
* @param int $maxRetries Maximum number of retries to find a valid value,
* After which an OverflowException is thrown.
*/
public function setValid(string $field, ?Closure $validator = null, int $maxRetries = 10000): static
{
$this->modifiedFields['valid'][$field] = compact('validator', 'maxRetries');

return $this;
}

/**
* Returns the current formatters
*/
Expand Down Expand Up @@ -380,7 +432,30 @@ public function makeArray()
$result = [];

foreach ($this->formatters as $field => $formatter) {
$result[$field] = $this->faker->{$formatter}();
$faker = $this->faker;

if (isset($this->modifiedFields['unique'][$field])) {
$faker = $faker->unique(
$this->modifiedFields['unique'][$field]['reset'],
$this->modifiedFields['unique'][$field]['maxRetries']
);
}

if (isset($this->modifiedFields['optional'][$field])) {
$faker = $faker->optional(
$this->modifiedFields['optional'][$field]['weight'],
$this->modifiedFields['optional'][$field]['default']
);
}

if (isset($this->modifiedFields['valid'][$field])) {
$faker = $faker->valid(
$this->modifiedFields['valid'][$field]['validator'],
$this->modifiedFields['valid'][$field]['maxRetries']
);
}

$result[$field] = $faker->format($formatter);
}
}
// If no formatters were defined then look for a model fake() method
Expand Down
2 changes: 1 addition & 1 deletion tests/system/Database/Live/FabricatorLiveTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public function testCreateAddsCountToDatabase(): void

// Some countries violate the 40 character limit so override that
$fabricator->setOverrides(['country' => 'France']);

$fabricator->setUnique('email');
$fabricator->create($count);

$this->seeNumRecords($count, 'user', []);
Expand Down
57 changes: 57 additions & 0 deletions tests/system/Test/FabricatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace CodeIgniter\Test;

use CodeIgniter\Config\Factories;
use CodeIgniter\Model;
use Tests\Support\Models\EntityModel;
use Tests\Support\Models\EventModel;
use Tests\Support\Models\FabricatorModel;
Expand Down Expand Up @@ -491,4 +492,60 @@ public function testResetClearsValue(): void

$this->assertSame(0, Fabricator::getCount('giants'));
}

public function testUniqueSetsOutUniqueFieldValues(): void
{
$model = new class () extends Model {
protected $allowedFields = ['email'];
protected $returnType = 'array';
};

$result = (new Fabricator($model))
->setUnique('email')
->make(5000);

$result = array_map(static fn (array $email): string => $email['email'], $result);

$this->assertSame(array_unique($result), $result);
}

public function testOptionalSetsOutOptionalFieldValues(): void
{
$model = new class () extends Model {
protected $allowedFields = ['email'];
protected $returnType = 'array';
};

$result = (new Fabricator($model))
->setOptional('email', 0.5, false) // 50% probability of email being `false`
->make(5000);

$result = array_map(static fn (array $email) => $email['email'], $result);

$this->assertLessThan(
count($result),
count(array_filter($result))
);
}

public function testValidSetsOutValidValuesUsingCallback(): void
{
$model = new class () extends Model {
protected $allowedFields = ['digit'];
protected $returnType = 'array';
};

$result = (new Fabricator($model, ['digit' => 'numberBetween']))
->setValid('digit', static fn (int $digit): bool => $digit % 2 === 0)
->make(5000);
$result = array_map(static fn (array $digit): int => $digit['digit'], $result);

foreach ($result as $digit) {
$this->assertSame(
0,
$digit % 2,
sprintf('Failed asserting that %s is even.', number_format($digit))
);
}
}
}
2 changes: 2 additions & 0 deletions user_guide_src/source/changelogs/v4.5.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ Testing
- **CLI:** The new ``InputOutput`` class was added and now you can write tests
for commands more easily if you use ``MockInputOutput``.
See :ref:`using-mock-input-output`.
- **Fabricator:** The Fabricator class now has the ``setUnique()``, ``setOptional()`` and ``setValid()``
methods to allow calling of Faker's modifiers on each field before faking their values.
- **TestResponse:** TestResponse no longer extends ``PHPUnit\Framework\TestCase`` as it
is not a test. Assertions' return types are now natively typed ``void``.

Expand Down
21 changes: 21 additions & 0 deletions user_guide_src/source/testing/fabricator.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,27 @@ a child class in your test support folder:

.. literalinclude:: fabricator/006.php

Setting Modifiers
=================

.. versionadded:: 4.5.0

Faker provides three special providers, ``unique()``, ``optional()``, and ``valid()``,
to be called before any provider. Fabricator fully supports these modifiers by providing
dedicated methods.

.. literalinclude:: fabricator/022.php

The arguments passed after the field name are passed directly to the modifiers as-is. You can refer
to `Faker's documentation on modifiers`_ for details.

.. _Faker's documentation on modifiers: https://fakerphp.github.io/#modifiers

Instead of calling each method on Fabricator, you may use Faker's modifiers directly if you are using
the ``fake()`` method on your models.

.. literalinclude:: fabricator/023.php

Localization
============

Expand Down
11 changes: 11 additions & 0 deletions user_guide_src/source/testing/fabricator/022.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

use App\Models\UserModel;
use CodeIgniter\Test\Fabricator;

$fabricator = new Fabricator(UserModel::class);
$fabricator->setUnique('email'); // sets generated emails to be always unique
$fabricator->setOptional('group_id'); // sets group id to be optional, with 50% chance to be `null`
$fabricator->setValid('age', static fn (int $age): bool => $age >= 18); // sets age to be 18 and above only

$users = $fabricator->make(10);
20 changes: 20 additions & 0 deletions user_guide_src/source/testing/fabricator/023.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace App\Models;

use CodeIgniter\Test\Fabricator;
use Faker\Generator;

class UserModel
{
protected $table = 'users';

public function fake(Generator &$faker)
{
return [
'first' => $faker->firstName(),
'email' => $faker->unique()->email(),
'group_id' => $faker->optional()->passthrough(mt_rand(1, Fabricator::getCount('groups'))),
];
}
}