# 生成测试数据

开发应用时，经常需要样本数据来运行测试。`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` 则会自动调用）。