生成测试数据

开发应用时,经常需要样本数据来运行测试。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.phpdefaultLocale 定义的值。 可通过现有 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 则会自动调用)。