Generating Test Data
Often you will need sample data for your application to run its tests. The Fabricator
class
uses Faker to turn models into generators
of random data. Use fabricators in your seeds or test cases to stage fake data for your unit tests.
Supported Models
Fabricator
supports any model that extends the framework’s core model, CodeIgniter\Model
.
You may use your own custom models by ensuring they implement CodeIgniter\Test\Interfaces\FabricatorModel
:
<?php
namespace App\Models;
use CodeIgniter\Test\Interfaces\FabricatorModel;
class MyModel implements FabricatorModel
{
public function find($id = null)
{
// TODO: Implement find() method.
}
public function insert($row = null, bool $returnID = true)
{
// TODO: Implement insert() method.
}
// ...
}
Note
In addition to methods, the interface outlines some necessary properties for the target model. Please see the interface code for details.
Loading Fabricators
At its most basic a fabricator takes the model to act on:
<?php
use App\Models\UserModel;
use CodeIgniter\Test\Fabricator;
$fabricator = new Fabricator(UserModel::class);
The parameter can be a string specifying the name of the model, or an instance of the model itself:
<?php
use App\Models\UserModel;
use CodeIgniter\Test\Fabricator;
$model = new UserModel($testDbConnection);
$fabricator = new Fabricator($model);
Defining Formatters
Faker generates data by requesting it from a formatter. With no formatters defined, Fabricator
will
attempt to guess at the most appropriate fit based on the field name and properties of the model it
represents, falling back on $fabricator->defaultFormatter
. This may be fine if your field names
correspond with common formatters, or if you don’t care much about the content of the fields, but most
of the time you will want to specify the formatters to use as the second parameter to the constructor:
<?php
use App\Models\UserModel;
use CodeIgniter\Test\Fabricator;
$formatters = [
'first' => 'firstName',
'email' => 'email',
'phone' => 'phoneNumber',
'avatar' => 'imageUrl',
];
$fabricator = new Fabricator(UserModel::class, $formatters);
You can also change the formatters after a fabricator is initialized by using the setFormatters()
method.
Advanced Formatting
Sometimes the default return of a formatter is not enough. Faker providers allow parameters to most formatters
to further limit the scope of random data. A fabricator will check its representative model for the fake()
method where you can define exactly what the faked data should look like:
<?php
namespace App\Models;
use Faker\Generator;
class UserModel
{
// ...
public function fake(Generator &$faker)
{
return [
'first' => $faker->firstName(),
'email' => $faker->email(),
'phone' => $faker->phoneNumber(),
'avatar' => \Faker\Provider\Image::imageUrl(800, 400),
'login' => config('Auth')->allowRemembering ? date('Y-m-d') : null,
];
/*
* Or you can return a return type object.
return new User([
'first' => $faker->firstName(),
'email' => $faker->email(),
'phone' => $faker->phoneNumber(),
'avatar' => \Faker\Provider\Image::imageUrl(800, 400),
'login' => config('Auth')->allowRemembering ? date('Y-m-d') : null,
]);
*/
}
}
Notice in this example how the first three values are equivalent to the formatters from before. However for avatar
we have requested an image size other than the default and login
uses a conditional based on app configuration,
neither of which are possible using the $formatters
parameter.
You may want to keep your test data separate from your production models, so it is a good practice to define a child class in your test support folder:
<?php
namespace Tests\Support\Models;
use App\Models\UserModel;
use Faker\Generator;
class UserFabricator extends UserModel
{
public function fake(Generator &$faker)
{
// ...
}
}
Setting Modifiers
Added in version 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.
<?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);
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.
Instead of calling each method on Fabricator, you may use Faker’s modifiers directly if you are using
the fake()
method on your models.
<?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'))),
];
}
}
Localization
Faker supports a lot of different locales. Check their documentation to determine which providers support your locale. Specify a locale in the third parameter while initiating a fabricator:
<?php
use App\Models\UserModel;
use CodeIgniter\Test\Fabricator;
$fabricator = new Fabricator(UserModel::class, null, 'fr_FR');
If no locale is specified it will use the one defined in app/Config/App.php as defaultLocale
.
You can check the locale of an existing fabricator using its getLocale()
method.
Faking the Data
Once you have a properly-initialized fabricator it is easy to generate test data with the make()
command:
<?php
use CodeIgniter\Test\Fabricator;
use Tests\Support\Models\UserFabricator;
$fabricator = new Fabricator(UserFabricator::class);
$testUser = $fabricator->make();
print_r($testUser);
You might get back something like this:
<?php
[
'first' => 'Maynard',
'email' => '[email protected]',
'phone' => '201-886-0269 x3767',
'avatar' => 'http://lorempixel.com/800/400/',
'login' => null,
];
You can also get a lot of them back by supplying a count:
<?php
$users = $fabricator->make(10);
The return type of make()
mimics what is defined in the representative model, but you can
force a type using the methods directly:
<?php
$userArray = $fabricator->makeArray();
$userObject = $fabricator->makeObject();
$userEntity = $fabricator->makeObject('App\Entities\User');
The return from make()
is ready to be used in tests or inserted into the database. Alternatively
Fabricator
includes the create()
command to insert it for you, and return the result. Due
to model callbacks, database formatting, and special keys like primary and timestamps the return
from create()
can differ from make()
. You might get back something like this:
<?php
[
'id' => 1,
'first' => 'Rachel',
'email' => '[email protected]',
'phone' => '741-241-2356',
'avatar' => 'http://lorempixel.com/800/400/',
'login' => null,
'created_at' => '2020-05-08 14:52:10',
'updated_at' => '2020-05-08 14:52:10',
];
Similar to make()
you can supply a count to insert and return an array of objects:
<?php
$users = $fabricator->create(100);
Finally, there may be times you want to test with the full database object but you are not actually
using a database. create()
takes a second parameter to allowing mocking the object, returning
the object with extra database fields above without actually touching the database:
<?php
$user = $fabricator(null, true);
$this->assertIsNumeric($user->id);
$this->dontSeeInDatabase('user', ['id' => $user->id]);
Specifying Test Data
Generated data is great, but sometimes you may want to supply a specific field for a test without
compromising your formatters configuration. Rather then creating a new fabricator for each variant
you can use setOverrides()
to specify the value for any fields:
<?php
$fabricator->setOverrides(['first' => 'Bobby']);
$bobbyUser = $fabricator->make();
Now any data generated with make()
or create()
will always use “Bobby” for the first
field:
<?php
[
'first' => 'Bobby',
'email' => '[email protected]',
'phone' => '251-806-2169',
'avatar' => 'http://lorempixel.com/800/400/',
'login' => null,
];
[
'first' => 'Bobby',
'email' => '[email protected]',
'phone' => '525-214-2656 x23546',
'avatar' => 'http://lorempixel.com/800/400/',
'login' => null,
];
setOverrides()
can take a second parameter to indicate whether this should be a persistent
override or only for a single action:
<?php
$fabricator->setOverrides(['first' => 'Bobby'], $persist = false);
$bobbyUser = $fabricator->make();
$bobbyUser = $fabricator->make();
Notice after the first return the fabricator stops using the overrides:
<?php
[
'first' => 'Bobby',
'email' => '[email protected]',
'phone' => '741-857-1933 x1351',
'avatar' => 'http://lorempixel.com/800/400/',
'login' => null,
];
[
'first' => 'Hans',
'email' => '[email protected]',
'phone' => '487-235-7006',
'avatar' => 'http://lorempixel.com/800/400/',
'login' => null,
];
If no second parameter is supplied then passed values will persist by default.
Test Helper
Often all you will need is a one-and-done fake object for testing. The Test Helper provides
the fake($model, $overrides, $persist = true)
function to do just this:
<?php
helper('test');
$user = fake('App\Models\UserModel', ['name' => 'Gerry']);
This is equivalent to:
<?php
use CodeIgniter\Test\Fabricator;
$fabricator = new Fabricator('App\Models\UserModel');
$fabricator->setOverrides(['name' => 'Gerry']);
$user = $fabricator->create();
If you just need a fake object without saving it to the database you can pass false into the persist parameter.
Table Counts
Frequently your faked data will depend on other faked data. Fabricator
provides a static
count of the number of faked items you have created for each table. Consider the following
example:
Your project has users and groups. In your test case you want to create various scenarios
with groups of different sizes, so you use Fabricator
to create a bunch of groups.
Now you want to create fake users but don’t want to assign them to a non-existent group ID.
Your model’s fake method could look like this:
<?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->email(),
'group_id' => mt_rand(1, Fabricator::getCount('groups')),
];
}
}
Now creating a new user will ensure it is a part of a valid group: $user = fake(UserModel::class);
Methods
Fabricator
handles the counts internally but you can also access these static methods
to assist with using them:
getCount(string $table): int
Return the current value for a specific table (default: 0).
setCount(string $table, int $count): int
Set the value for a specific table manually, for example if you create some test items without using a fabricator that you still wanted factored into the final counts.
upCount(string $table): int
Increment the value for a specific table by one and return the new value. (This is what is
used internally with Fabricator::create()
).
downCount(string $table): int
Decrement the value for a specific table by one and return the new value, for example if you deleted a fake item but wanted to track the change.
resetCounts()
Resets all counts. Good idea to call this between test cases (though using
CIUnitTestCase::$refresh = true
does it automatically).