Skip to content

Commit

Permalink
Merge pull request #8671 from paulbalandan/fabricator-modifiers
Browse files Browse the repository at this point in the history
feat: Support faker modifiers on Fabricator
  • Loading branch information
kenjis authored Mar 30, 2024
2 parents ebadd4c + 31afcfa commit 3574196
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 2 deletions.
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'))),
];
}
}

0 comments on commit 3574196

Please sign in to comment.