生成测试数据
开发应用时,经常需要样本数据来运行测试。Fabricator 类
借助 Faker 将模型转化为随机数据生成器。
可在 Seed 文件或测试用例中使用 Fabricator 来生成模拟数据,用于单元测试。
支持的模型
Fabricator 支持任何继承自框架核心模型 CodeIgniter\Model 的模型。
如需使用自定义模型,确保其实现了 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.
}
// ...
}
备注
除方法外,该接口还定义了一些目标模型所需的属性。详见接口源码。
加载 Fabricator
最基本的用法是传入要操作的模型:
<?php
use App\Models\UserModel;
use CodeIgniter\Test\Fabricator;
$fabricator = new Fabricator(UserModel::class);
参数可以是指定模型名称的字符串,也可以是模型实例:
<?php
use App\Models\UserModel;
use CodeIgniter\Test\Fabricator;
$model = new UserModel($testDbConnection);
$fabricator = new Fabricator($model);
定义格式化器
Faker 通过格式化器来生成数据。未指定格式化器时,Fabricator 会
根据字段名和模型属性自动推测最合适的格式化器,
若推测失败则回退到 $fabricator->defaultFormatter。
如果字段名恰好对应常见格式化器,或者不关心字段内容,自动推测可能够用,
但多数情况下需要通过构造函数的第二个参数明确指定:
<?php
use App\Models\UserModel;
use CodeIgniter\Test\Fabricator;
$formatters = [
'first' => 'firstName',
'email' => 'email',
'phone' => 'phoneNumber',
'avatar' => 'imageUrl',
];
$fabricator = new Fabricator(UserModel::class, $formatters);
也可以在 Fabricator 初始化后通过 setFormatters() 方法更改格式化器。
高级格式化
有时格式化器的默认返回值并不够用。Faker Provider 的大多数格式化器都支持接收参数,从而进一步缩小随机数据的生成范围。Fabricator 会检查关联模型中是否定义了 fake() 方法,以便在该方法中精确定义模拟数据的格式:
<?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,
]);
*/
}
}
在此示例中,前三个值与先前的格式化器完全等效。但 avatar 请求了非默认的图像尺寸,login 则根据应用配置使用了条件逻辑,这两项需求均无法通过 $formatters 参数实现。
为了将测试数据与生产模型分离,最佳实践是在测试支持目录中定义一个子类:
<?php
namespace Tests\Support\Models;
use App\Models\UserModel;
use Faker\Generator;
class UserFabricator extends UserModel
{
public function fake(Generator &$faker)
{
// ...
}
}
设置修饰器
Added in version 4.5.0.
Faker 提供三个特殊 Provider unique()、optional() 和 valid(),
可在任何 Provider 之前调用。Fabricator 通过专用方法完全支持这些修饰器。
<?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);
字段名之后的参数会原样传递给修饰器。详见 Faker 修饰器文档。
除了调用 Fabricator 的方法外,如果模型使用了 fake() 方法,
也可以直接使用 Faker 的修饰器。
<?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'))),
];
}
}
本地化
Faker 支持多种语言环境。查阅 Faker 文档了解哪些 Provider 支持所需语言环境。 初始化 Fabricator 时,通过第三个参数指定语言环境:
<?php
use App\Models\UserModel;
use CodeIgniter\Test\Fabricator;
$fabricator = new Fabricator(UserModel::class, null, 'fr_FR');
未指定语言环境时,将使用 app/Config/App.php 中 defaultLocale 定义的值。
可通过现有 Fabricator 的 getLocale() 方法检查其语言环境。
生成模拟数据
正确初始化 Fabricator 后,使用 make() 命令即可轻松生成测试数据:
<?php
use CodeIgniter\Test\Fabricator;
use Tests\Support\Models\UserFabricator;
$fabricator = new Fabricator(UserFabricator::class);
$testUser = $fabricator->make();
print_r($testUser);
返回值可能如下:
<?php
[
'first' => 'Maynard',
'email' => 'king.alford@example.org',
'phone' => '201-886-0269 x3767',
'avatar' => 'http://lorempixel.com/800/400/',
'login' => null,
];
传入数量参数可批量生成:
<?php
$users = $fabricator->make(10);
make() 的返回类型与模型定义的保持一致,也可以通过对应方法强制指定类型:
<?php
$userArray = $fabricator->makeArray();
$userObject = $fabricator->makeObject();
$userEntity = $fabricator->makeObject('App\Entities\User');
make() 的返回值可直接用于测试或手动插入数据库。此外,Fabricator 提供的 create() 方法可自动执行插入操作并返回结果。由于受模型回调、数据库格式化以及主键、时间戳等特殊字段的影响,create() 的返回值可能与 make() 有所不同。返回结果示例如下:
<?php
[
'id' => 1,
'first' => 'Rachel',
'email' => 'bradley72@gmail.com',
'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',
];
与 make() 类似,可传入数量参数来批量执行插入并返回对象数组:
<?php
$users = $fabricator->create(100);
最后,若需在不实际使用数据库的情况下,使用完整的数据库对象进行测试,可为 create() 传入第二个参数来 Mock 对象。该操作无需访问数据库,即可返回包含上述额外数据库字段的对象:
<?php
$user = $fabricator(null, true);
$this->assertIsNumeric($user->id);
$this->dontSeeInDatabase('user', ['id' => $user->id]);
指定测试数据
自动生成的数据固然方便,但有时需要在测试中为特定字段指定数值,同时又不希望改动原有的格式化器配置。无需为每种变体重复创建 Fabricator,通过 setOverrides() 方法即可为任意字段指定覆盖值:
<?php
$fabricator->setOverrides(['first' => 'Bobby']);
$bobbyUser = $fabricator->make();
现在使用 make() 或 create() 生成的任何数据,first 字段始终为 "Bobby":
<?php
[
'first' => 'Bobby',
'email' => 'latta.kindel@company.org',
'phone' => '251-806-2169',
'avatar' => 'http://lorempixel.com/800/400/',
'login' => null,
];
[
'first' => 'Bobby',
'email' => 'melissa.strike@fabricon.us',
'phone' => '525-214-2656 x23546',
'avatar' => 'http://lorempixel.com/800/400/',
'login' => null,
];
setOverrides() 接受第二个参数,用于指定该覆盖是持久的还是仅单次生效:
<?php
$fabricator->setOverrides(['first' => 'Bobby'], $persist = false);
$bobbyUser = $fabricator->make();
$bobbyUser = $fabricator->make();
注意第一次返回后 Fabricator 就不再使用覆盖值:
<?php
[
'first' => 'Bobby',
'email' => 'belingadon142@example.org',
'phone' => '741-857-1933 x1351',
'avatar' => 'http://lorempixel.com/800/400/',
'login' => null,
];
[
'first' => 'Hans',
'email' => 'hoppifur@metraxalon.com',
'phone' => '487-235-7006',
'avatar' => 'http://lorempixel.com/800/400/',
'login' => null,
];
未传入第二个参数时,默认覆盖值会持续生效。
测试辅助函数
很多时候测试只需要一个一次性的模拟对象。测试辅助函数提供
fake($model, $overrides, $persist = true) 函数来实现此功能:
<?php
helper('test');
$user = fake('App\Models\UserModel', ['name' => 'Gerry']);
等价于:
<?php
use CodeIgniter\Test\Fabricator;
$fabricator = new Fabricator('App\Models\UserModel');
$fabricator->setOverrides(['name' => 'Gerry']);
$user = $fabricator->create();
如果只需要模拟对象而不想保存到数据库,可将 persist 参数设为 false。
表计数
模拟数据之间往往存在依赖关系。Fabricator 针对每张表维护了已生成模拟数据总数的静态计数。
以“用户与组”的关系为例:若测试场景中需要创建不同规模的分组,可先利用 Fabricator 生成一批分组。随后在创建模拟用户时,为确保关联到有效的组 ID,可在模型的 fake() 方法中按如下方式编写:
<?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')),
];
}
}
此时创建新用户即可确保其属于有效分组:$user = fake(UserModel::class);
方法
Fabricator 内部管理计数,同时提供以下静态方法
方便使用:
getCount(string $table): int
返回指定表的当前计数值(默认:0)。
setCount(string $table, int $count): int
手动设置指定表的计数值,例如创建了一些未通过 Fabricator 生成的 测试条目,但仍想将其计入总数。
upCount(string $table): int
将指定表的计数加一并返回新值。(Fabricator::create() 内部使用此方法)。
downCount(string $table): int
将指定表的计数减一并返回新值,例如删除了某个模拟条目 但又想追踪此变更。
resetCounts()
重置所有计数。建议在测试用例之间调用(如果设置了
CIUnitTestCase::$refresh = true 则会自动调用)。