Skip to content

Commit 3574196

Browse files
authored
Merge pull request #8671 from paulbalandan/fabricator-modifiers
feat: Support faker modifiers on Fabricator
2 parents ebadd4c + 31afcfa commit 3574196

File tree

7 files changed

+188
-2
lines changed

7 files changed

+188
-2
lines changed

system/Test/Fabricator.php

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace CodeIgniter\Test;
1515

16+
use Closure;
1617
use CodeIgniter\Exceptions\FrameworkException;
1718
use CodeIgniter\I18n\Time;
1819
use CodeIgniter\Model;
@@ -88,6 +89,17 @@ class Fabricator
8889
*/
8990
protected $tempOverrides;
9091

92+
/**
93+
* Fields to be modified before applying any formatter.
94+
*
95+
* @var array{
96+
* unique: array<non-empty-string, array{reset: bool, maxRetries: int}>,
97+
* optional: array<non-empty-string, array{weight: float, default: mixed}>,
98+
* valid: array<non-empty-string, array{validator: Closure(mixed): bool|null, maxRetries: int}>
99+
* }
100+
*/
101+
private array $modifiedFields = ['unique' => [], 'optional' => [], 'valid' => []];
102+
91103
/**
92104
* Default formatter to use when nothing is detected
93105
*
@@ -251,6 +263,46 @@ public function setOverrides(array $overrides = [], $persist = true): self
251263
return $this;
252264
}
253265

266+
/**
267+
* Set a field to be unique.
268+
*
269+
* @param bool $reset If set to true, resets the list of existing values
270+
* @param int $maxRetries Maximum number of retries to find a unique value,
271+
* After which an OverflowException is thrown.
272+
*/
273+
public function setUnique(string $field, bool $reset = false, int $maxRetries = 10000): static
274+
{
275+
$this->modifiedFields['unique'][$field] = compact('reset', 'maxRetries');
276+
277+
return $this;
278+
}
279+
280+
/**
281+
* Set a field to be optional.
282+
*
283+
* @param float $weight A probability between 0 and 1, 0 means that we always get the default value.
284+
*/
285+
public function setOptional(string $field, float $weight = 0.5, mixed $default = null): static
286+
{
287+
$this->modifiedFields['optional'][$field] = compact('weight', 'default');
288+
289+
return $this;
290+
}
291+
292+
/**
293+
* Set a field to be valid using a callback.
294+
*
295+
* @param Closure(mixed): bool|null $validator A function returning true for valid values
296+
* @param int $maxRetries Maximum number of retries to find a valid value,
297+
* After which an OverflowException is thrown.
298+
*/
299+
public function setValid(string $field, ?Closure $validator = null, int $maxRetries = 10000): static
300+
{
301+
$this->modifiedFields['valid'][$field] = compact('validator', 'maxRetries');
302+
303+
return $this;
304+
}
305+
254306
/**
255307
* Returns the current formatters
256308
*/
@@ -380,7 +432,30 @@ public function makeArray()
380432
$result = [];
381433

382434
foreach ($this->formatters as $field => $formatter) {
383-
$result[$field] = $this->faker->{$formatter}();
435+
$faker = $this->faker;
436+
437+
if (isset($this->modifiedFields['unique'][$field])) {
438+
$faker = $faker->unique(
439+
$this->modifiedFields['unique'][$field]['reset'],
440+
$this->modifiedFields['unique'][$field]['maxRetries']
441+
);
442+
}
443+
444+
if (isset($this->modifiedFields['optional'][$field])) {
445+
$faker = $faker->optional(
446+
$this->modifiedFields['optional'][$field]['weight'],
447+
$this->modifiedFields['optional'][$field]['default']
448+
);
449+
}
450+
451+
if (isset($this->modifiedFields['valid'][$field])) {
452+
$faker = $faker->valid(
453+
$this->modifiedFields['valid'][$field]['validator'],
454+
$this->modifiedFields['valid'][$field]['maxRetries']
455+
);
456+
}
457+
458+
$result[$field] = $faker->format($formatter);
384459
}
385460
}
386461
// If no formatters were defined then look for a model fake() method

tests/system/Database/Live/FabricatorLiveTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public function testCreateAddsCountToDatabase(): void
5151

5252
// Some countries violate the 40 character limit so override that
5353
$fabricator->setOverrides(['country' => 'France']);
54-
54+
$fabricator->setUnique('email');
5555
$fabricator->create($count);
5656

5757
$this->seeNumRecords($count, 'user', []);

tests/system/Test/FabricatorTest.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace CodeIgniter\Test;
1515

1616
use CodeIgniter\Config\Factories;
17+
use CodeIgniter\Model;
1718
use Tests\Support\Models\EntityModel;
1819
use Tests\Support\Models\EventModel;
1920
use Tests\Support\Models\FabricatorModel;
@@ -491,4 +492,60 @@ public function testResetClearsValue(): void
491492

492493
$this->assertSame(0, Fabricator::getCount('giants'));
493494
}
495+
496+
public function testUniqueSetsOutUniqueFieldValues(): void
497+
{
498+
$model = new class () extends Model {
499+
protected $allowedFields = ['email'];
500+
protected $returnType = 'array';
501+
};
502+
503+
$result = (new Fabricator($model))
504+
->setUnique('email')
505+
->make(5000);
506+
507+
$result = array_map(static fn (array $email): string => $email['email'], $result);
508+
509+
$this->assertSame(array_unique($result), $result);
510+
}
511+
512+
public function testOptionalSetsOutOptionalFieldValues(): void
513+
{
514+
$model = new class () extends Model {
515+
protected $allowedFields = ['email'];
516+
protected $returnType = 'array';
517+
};
518+
519+
$result = (new Fabricator($model))
520+
->setOptional('email', 0.5, false) // 50% probability of email being `false`
521+
->make(5000);
522+
523+
$result = array_map(static fn (array $email) => $email['email'], $result);
524+
525+
$this->assertLessThan(
526+
count($result),
527+
count(array_filter($result))
528+
);
529+
}
530+
531+
public function testValidSetsOutValidValuesUsingCallback(): void
532+
{
533+
$model = new class () extends Model {
534+
protected $allowedFields = ['digit'];
535+
protected $returnType = 'array';
536+
};
537+
538+
$result = (new Fabricator($model, ['digit' => 'numberBetween']))
539+
->setValid('digit', static fn (int $digit): bool => $digit % 2 === 0)
540+
->make(5000);
541+
$result = array_map(static fn (array $digit): int => $digit['digit'], $result);
542+
543+
foreach ($result as $digit) {
544+
$this->assertSame(
545+
0,
546+
$digit % 2,
547+
sprintf('Failed asserting that %s is even.', number_format($digit))
548+
);
549+
}
550+
}
494551
}

user_guide_src/source/changelogs/v4.5.0.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ Testing
4444
- **CLI:** The new ``InputOutput`` class was added and now you can write tests
4545
for commands more easily if you use ``MockInputOutput``.
4646
See :ref:`using-mock-input-output`.
47+
- **Fabricator:** The Fabricator class now has the ``setUnique()``, ``setOptional()`` and ``setValid()``
48+
methods to allow calling of Faker's modifiers on each field before faking their values.
4749
- **TestResponse:** TestResponse no longer extends ``PHPUnit\Framework\TestCase`` as it
4850
is not a test. Assertions' return types are now natively typed ``void``.
4951

user_guide_src/source/testing/fabricator.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,27 @@ a child class in your test support folder:
6262

6363
.. literalinclude:: fabricator/006.php
6464

65+
Setting Modifiers
66+
=================
67+
68+
.. versionadded:: 4.5.0
69+
70+
Faker provides three special providers, ``unique()``, ``optional()``, and ``valid()``,
71+
to be called before any provider. Fabricator fully supports these modifiers by providing
72+
dedicated methods.
73+
74+
.. literalinclude:: fabricator/022.php
75+
76+
The arguments passed after the field name are passed directly to the modifiers as-is. You can refer
77+
to `Faker's documentation on modifiers`_ for details.
78+
79+
.. _Faker's documentation on modifiers: https://fakerphp.github.io/#modifiers
80+
81+
Instead of calling each method on Fabricator, you may use Faker's modifiers directly if you are using
82+
the ``fake()`` method on your models.
83+
84+
.. literalinclude:: fabricator/023.php
85+
6586
Localization
6687
============
6788

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
use App\Models\UserModel;
4+
use CodeIgniter\Test\Fabricator;
5+
6+
$fabricator = new Fabricator(UserModel::class);
7+
$fabricator->setUnique('email'); // sets generated emails to be always unique
8+
$fabricator->setOptional('group_id'); // sets group id to be optional, with 50% chance to be `null`
9+
$fabricator->setValid('age', static fn (int $age): bool => $age >= 18); // sets age to be 18 and above only
10+
11+
$users = $fabricator->make(10);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use CodeIgniter\Test\Fabricator;
6+
use Faker\Generator;
7+
8+
class UserModel
9+
{
10+
protected $table = 'users';
11+
12+
public function fake(Generator &$faker)
13+
{
14+
return [
15+
'first' => $faker->firstName(),
16+
'email' => $faker->unique()->email(),
17+
'group_id' => $faker->optional()->passthrough(mt_rand(1, Fabricator::getCount('groups'))),
18+
];
19+
}
20+
}

0 commit comments

Comments
 (0)