factory-bot / factory-bot
使用简单的定义语法替换 fixtures
Requires
- php: >=5.4.0
Requires (Dev)
Suggests
- fzaninotto/faker: For generating fake data in entity definitions
README
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 支持几种不同的构建策略:build
,create
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") ] );
依赖属性
可以使用可调用定义样式根据其他属性的值来设置属性。可调用函数接收部分填充的模型。模型按指定顺序填充指定的参数,因此将电子邮件放在最后使我们能够访问 firstName
和 lastName
。
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(); }, ] ); } }