factory-bot/factory-bot

使用简单的定义语法替换 fixtures

1.4.1 2021-10-06 10:55 UTC

This package is auto-updated.

Last update: 2024-09-06 18:59:56 UTC


README

Version PHP Composer Coverage Maintainability

FactoryBot

这是由 thoughtbot 的 ruby factory_bot 启发的。

FactoryBot 是一个 fixtures 替换,具有简单的定义语法,支持多种构建策略(已保存的实例,未保存的实例),以及支持同一类的多个工厂(用户、admin_user 等),包括工厂继承。

目录

安装

使用以下命令安装最新版本:

    $ composer require factory-bot/factory-bot

定义工厂

每个工厂都有一个名称和一组属性。默认情况下,名称用于猜测对象的类

use FactoryBot\FactoryBot;

FactoryBot::define(
    UserModel::class,
    ["firstName" => "John", "lastName" => "Doe"]
);

也可以显式指定类

use FactoryBot\FactoryBot;

FactoryBot::define(
    "Admin",
    ["firstName" => "John", "lastName" => "Doe"],
    ["class" => UserModel::class]
);

强烈建议您为每个类提供一个工厂,该工厂提供创建该类实例所需的最简单的属性集。其他工厂可以通过继承创建,以覆盖每个类的常见场景。

尝试定义具有相同名称的多个工厂将覆盖之前定义的工厂。

加载定义

定义定义的默认路径是 tests/factories。您可以为要为工厂定义的每个模型创建一个文件。例如,您可以为 tests/factories/UserModel.php 文件定义一个基础工厂和从基础工厂扩展的不同工厂。

您的文件结构可能如下所示

.
├── src
├── tests
│   ├── factories
│   │   ├── UserModel.php    # Factory definitions for UserModel
│   │   └── AccountModel.php # Factory definitions for AccountModel
│   ├── ... # tests
│   └── ... # tests
└── README.md

在您的测试文件中使用工厂之前,您可以通过调用 FactoryBot::findDefinitions(); 加载它们。

如果您使用 PHPUnit,则可以在您的引导文件中这样做。

如果您需要将工厂保存到项目中的不同位置,您可以使用 FactoryBot::setDefinitionsBasePath("your/path/factories/"); 指定它。

您还有可能内联定义工厂并省略调用 findDefinitions

使用工厂

FactoryBot 支持几种不同的构建策略:buildcreate

use FactoryBot\FactoryBot;

# Returns a User instance that's not saved
FactoryBot::build(UserModel::class)

# Returns a saved User instance
FactoryBot::create(UserModel::class);

无论使用哪种策略,都可以通过传递一个数组来覆盖定义的属性

# Build a User instance and override the first_name property
FactoryBot::build(UserModel::class, ["firstName" => "Jane"]);

别名

FactoryBot 允许您定义现有工厂的别名,以便更容易重用。例如,当您的 Post 对象的 author 属性实际上指向 User 类的实例时,这可能很有用。虽然通常 FactoryBot 可以从关联名称推断出工厂名称,但在这种情况下,它将徒劳地寻找 author 工厂。因此,为用户工厂提供别名,以便可以使用别名名称。

FactoryBot::define(
    UserModel::class,
    ["firstName" => "Jane"],
    ["aliases" => ["Author", "Commenter"]]
);

FactoryBot::define(
    PostModel::class,
    [
        "title" => "lorem ipsum!",
        "body" => "lorem ipsum dolor sit amet",
        "author" => FactoryBot::relation("Author")
    ]
);

FactoryBot::define(
    CommentModel::class,
    [
        "body" => "lorem ipsum dolor sit amet",
        "commenter" => FactoryBot::relation("Commenter")
    ]
);

依赖属性

可以使用可调用定义样式根据其他属性的值来设置属性。可调用函数接收部分填充的模型。模型按指定顺序填充指定的参数,因此将电子邮件放在最后使我们能够访问 firstNamelastName

FactoryBot::define(
    UserModel::class,
    [
        "firstName" => "John",
        "lastName" => "Doe",
        "email" => function ($model) {
            return strtolower(
                $model->getfirstName() . "." . $model->getLastName() . "@example.com"
            );
        }
    ]
);

FactoryBot::build(UserModel::class)->getEmail()
# > "john.doe@example.com"

继承

您可以通过嵌套工厂轻松为同一类创建多个工厂,而无需重复公共属性

# Define a basic user
FactoryBot::define(
    UserModel::class,
    [
        "firstName" => "Jane",
        "lastName" => "Doe",
        "role" => "user"
    ]
);

# Extend the User Model as an Admin Factory
FactoryBot::extend("Admin", UserModel::class, ["role" => "admin"]);

如上所述,为每个类定义一个仅包含创建其所需的属性的基本工厂是良好实践。然后,创建更多特定于类的工厂,这些工厂从基本父类继承。工厂定义仍然是代码,因此请保持它们 DRY。

关系

您可以在工厂内部设置关系。

关系默认使用与父对象相同的构建策略。

FactoryBot::define(
    PostModel::class,
    ["author" => FactoryBot::relation("Author")]
);

$post = FactoryBot::create(PostModel::class);
$post->isNew();              # > false
$post->getAuthor()->isNew(); # > false

$post2 = FactoryBot::build(PostModel::class);
$post2->isNew();              # > true
$post2->getAuthor()->isNew(); # > true

单个关系

使用FactoryBot的关系方法,并指定应使用的Factory。

FactoryBot::define(
    PostModel::class,
    [
        "title" => "lorem ipsum!",
        "body" => "lorem ipsum dolor sit amet",
        "author" => FactoryBot::relation("Author")
    ]
);

多个关系

要生成“拥有多个”关系,可以使用关系方法。

FactoryBot::define(
    UserModel::class,
    ["posts" => FactoryBot::relations(PostModel::class, 2)]
);

$user = FactoryBot::build(UserModel::class);
$user->getPosts(); # > [PostModel, PostModel]

循环关系

要生成循环关系,应将子对象设置为null以避免无限生成子对象。

FactoryBot::define(
    UserModel::class,
    [
        "firstName" => "Jane",
        "lastName" => "Doe",
        "subordinate" => FactoryBot::relation(
            UserModel::class,
            ["subordinate" => null]
        )
    ]
);

$user = FactoryBot::build(UserModel::class);
$user->getSubordinate(); # > UserModel
$user->getSubordinate()->getSubordinate(); # > null

共享依赖

如果您想在Factory定义中共享依赖实例,可以使用闭包风格定义。这对于高级用例可能很有用,但它增加了定义的复杂性,因此建议仅在扩展Factory上使用此定义风格。

FactoryBot::extend(
    "Supervisor",
    UserModel::class,
    [
        "company" => FactoryBot::relation(CompanyModel::class),
        "subordinate" => function ($user, $buildStrategy) {
            return FactoryBot::relation(
                UserModel::class,
                [
                    "company" => $user->getCompany(), # subordinate works in the same company as its supervisor
                    "subordinate" => null
                ]
            )(null, $buildStrategy); # call the relation with null and the parent's build strategy
        }
    ]
);

请记住,您也可以在创建测试数据时手动共享模型。这也帮助开发者更容易地理解测试用例的依赖关系。

$company = FactoryBot::build(CompanyModel::class);
$subordinate = FactoryBot::build(UserModel::class, ["subordinate" => null, "company" => $company]);
$user = FactoryBot::build(UserModel::class, ["subordinate" => $subordinate, "company" => $company]);

序列

可以使用序列生成特定格式(例如,电子邮件地址)的唯一值。

默认实现将生成数字序列,类似于SQL中的经典自增。

FactoryBot::define(UserModel::class, ["id" => FactoryBot::sequence()]);

要实现自己的序列方法,传递一个每次调用生成唯一序列值的函数。

FactoryBot::define(
    UserModel::class,
    [
        "email" => FactoryBot::sequence(function($num, $model) {
            return "user" . $num . "@example.com";
        })
    ]
);

$user = FactoryBot::build(UserModel::class);
$user->getEmail() # > "user1@example.com"

自定义策略

可以通过实现自定义策略来扩展FactoryBot的功能。

策略必须实现FactoryBot/Strategies/StrategyInterface接口。接口定义了两个方法。beforeCompile是在执行水化步骤之前调用的方法。第二个方法是result,它在水化步骤之后调用,在这里您可以在返回实例之前对其进行一些更改。

在这个例子中,策略将返回实例的JSON字符串。

use FactoryBot\FactoryBot;
use FactoryBot\Strategies\StrategyInterface;

class JsonStrategy implements StrategyInterface
{
    public static function beforeCompile($factory)
    {
        // we call $factory->notify to ensure the Factories "before" Hooks get called
        $factory->notify("before");
    }

    public static function result($factory, $instance)
    {
        // we call $factory->notify to ensure the Factories "after" Hooks get called
        $factory->notify("after");

        $result = self::getPropertiesArray($instance);

        return json_encode($result);
    }

    public static function getPropertiesArray($instance)
    {
        $instanceArray = (array) $instance;
        $result = [];
        foreach ($instanceArray as $keyWithVisibility => $value) {
            $keySegments = explode("\0", $keyWithVisibility);
            $keyWithoutVisibility = end($keySegments);
            $result[$keyWithoutVisibility] = $value;
        }
        return $result;
    }
}

FactoryBot::registerStrategy("json", JsonStrategy::class);
FactoryBot::define(UserModel::class, ["firstName" => "Jane"]);
FactoryBot::json(UserModel::class); # > '{"id":null,"firstName":"Jane",...,"subordinate":null,"new":true}'

生命周期钩子

FactoryBot提供了6个不同的钩子来注入自定义代码。

  • before - 在构建或创建实例之前调用。
  • after - 在构建或创建实例之后调用。
  • beforeCreate - 在创建实例之前调用。
  • afterCreate - 在创建实例之后调用。
  • beforeBuild - 在构建实例之前调用。
  • afterBuild - 在构建实例之后调用。

Factory可以定义任意数量的此类钩子。这些钩子将按指定的顺序执行。

每个工厂的钩子

在Factory定义中,可以注册生命周期钩子,该钩子仅在定义它的Factory中执行。

示例

$logger = new Logger();

FactoryBot::define(
    UserModel::class,
    ["name" => "Jane Doe"],
    ["hooks" => [
        FactoryBot::hook("afterCreate", function ($instance) use ($logger) {
            $logger->debug("created an UserModel instance: $instance->getName()");
        })
    ]]
);

$user = FactoryBot::create(UserModel::class); # logger output > "created an UserModel instance: Jane Doe"

全局钩子

可以为所有Factory注册钩子。

$logger = new Logger();

FactoryBot::registerGlobalHook("afterCreate", function ($instance) use ($logger) {
    $class = get_class($instance);
    $logger->debug("created an $class instance");
});

FactoryBot::define(UserModel::class);
FactoryBot::define(PostModel::class);

$user = FactoryBot::create(UserModel::class); # logger output > "created an UserModel instance"
$post = FactoryBot::create(PostModel::class); # logger output > "created an PostModel instance"

钩子也可以被移除。

$logger = new Logger();

$hook = FactoryBot::registerGlobalHook("afterCreate", function ($instance) use ($logger) {
    $class = get_class($instance);
    $logger->debug("created an $class instance");
});

FactoryBot::define(UserModel::class);
FactoryBot::define(PostModel::class);

$user = FactoryBot::create(UserModel::class); # logger output > "created an UserModel instance"
$post = FactoryBot::create(PostModel::class); # logger output > "created an PostModel instance"

FactoryBot::removeGlobalHook($hook);

$user = FactoryBot::create(UserModel::class); # no logger output
$post = FactoryBot::create(PostModel::class); # no logger output

使用 FactoryBot 与 php faker

Faker方法应被可调用对象包裹。这样,Faker方法将在构建过程中被调用。否则,Faker方法将在定义时被调用,所有实例都将具有相同的值。

使用Faker的本地实例

use Faker\Factory;
use FactoryBot\FactoryBot;

$faker = Factory::create('at_AT');
FactoryBot::define(
    UserModel::class,
    [
        "name" => "Jane Doe",
        # local variables have to be injected using use
        "street" => function () use ($faker) { return $faker->streetName(); },
    ]
);

使用实例变量

use Faker\Factory;
use FactoryBot\FactoryBot;

class FactorySetup
{
    /**
     * faker generator
    */
    private $faker;

    public function __construct()
    {
        $this->faker = Factory::create('at_AT');
        $this->setUpUserFactory()
    }

    public function setUpUserFactory()
    {
        FactoryBot::define(
            UserModel::class,
            [
                "name" => "Jane Doe",
                # instance variables can be accessed without use
                "street" => function () { return $this->faker->streetName(); },
            ]
        );
    }
}