该软件包已被废弃,不再维护。未建议替代软件包。

关于poser的描述。

安装次数: 53,638

依赖项: 0

建议者: 0

安全性: 0

星标: 283

关注者: 5

分支: 8

开放问题: 2

类型:软件包

4.2.0 2020-09-11 13:22 UTC

README

poser-logo.png

Poser

All Contributors

Software License

Laravel 类型化模型工厂仅需几秒钟!编写看起来像这样...的测试...

/** @test */
public function a_user_can_have_customers()
{
    UserFactory::times(20)
               ->hasAddress()
               ->withCustomers(CustomerFactory::times(20)->withBooks(5))();

    $this->assertCount(20 * 20 * 5, Book::all());
}

...使用这样的 Factory...

namespace Tests\Factories;

use Lukeraymonddowning\Poser\Factory;

class UserFactory extends Factory {

    // No need to write any code here
    
}

针对 Laravel 8 用户的说明

虽然 Poser 被标记为 Laravel 8 兼容,但您应该开始迁移以使用 Laravel 的新内置类类型工厂。语法几乎相同,您会很快上手。我要感谢所有使用、测试、修改和讨论 Poser 的人。为 Laravel 社区建设某物真是太有趣了!

示例

想看看 Poser 与默认 Laravel 测试相比的样子吗?查看我们的示例!

安装

首先,使用 composer 将其安装到您的 Laravel 项目中。

composer require lukeraymonddowning/poser

接下来,通过调用以下命令发布 Poser 配置文件:

php artisan vendor:publish --tag=poser

Lumen 安装

在您的配置目录中创建 poser.php 并将 lukeraymonddowning/poser/src/config/poser.php 复制到其中

$app->configure('poser'); 添加到 boostrap/app.php 中的配置部分

$app->register(\Lukeraymonddowning\Poser\PoserServiceProvider::class); 添加到 boostrap/app.php 中的提供者部分

入门

要快速入门,我们提供了一个 php artisan make:poser 命令。您可以将您工厂的期望名称作为参数传递。因此,创建 UserFactory 的命令将是 php artisan make:poser UserFactory

如果您想让 Poser 做所有的工作,只需调用 php artisan make:poser,将您的 poser.models_namespace 配置条目中定义的所有模型转换为 Poser 工厂。

更倾向于视觉的人?观看 Poser 的视频演示

使用方法

Poser 从编写 基于类的模型工厂 的样板中删除了所有内容。要开始,请安装 Poser 并转到您的测试套件。请注意:Poser 使用数据库(显然),所以请确保您的测试类扩展 Laravel 的 TestCase,而不是 PhpUnit 的。

基础知识

让我们假设您有一个具有多个客户的用户模型...

<?php

namespace App;

class User extends Authenticatable
{

    // ...a little while later

    public function customers()
    {
        return $this->hasMany(Customer::class);
    }
}

要设置此工厂,创建一个名为 UserFactory 的类(我们建议在 'tests' 文件夹中创建一个 'Factories' 目录),并一个名为 CustomerFactory 的类。

这两个类都应该扩展Poser/Factory抽象类。Poser可以通过make:poser命令为您处理,因此您可以调用php artisan make:poser UserFactoryphp artisan make:poser CustomerFactory

您还应该在您的database/factories目录中添加CustomerFactoryUserFactory作为条目(标准Laravel内容

现在,前往您要编写的测试,并输入以下内容

/** @test */
public function user_has_customers()
{
    $user = UserFactory::new()
        ->withCustomers(CustomerFactory::times(30))
        ->create();

    $this->assertCount(30, $user->customers);
}

测试应该轻松通过。太棒了!请注意,我们不需要实现withCustomers()方法:Poser能够智能地决定我们想要做什么。

对于HasOneHasMany关系,您只需在模型中的关系方法前加with即可(例如:User模型中的customers()方法在测试中变为withCustomers),然后Poser会完成剩下的工作。

让我们添加一点复杂性:每位客户可以拥有多本书...

class Customer extends Model
{

    public function books()
    {
        return $this->hasMany(Book::class);
    }

    public function user()
    {
        return $this->belongsTo(User::class);
    }

}

到目前为止,一切顺利。让我们创建另一个工厂类,这次称为BookFactory,它再次扩展Poser的抽象Factory类。就这么简单!修改您的原始测试,为我们的每位客户分配5本书...

/** @test */
public function user_has_customers()
{
    $user = UserFactory::new()
        ->withCustomers(
            CustomerFactory::times(30)->withBooks(BookFactory::times(5))
        )
        ->create();

    $this->assertCount(30, $user->customers);
    $this->assertCount(150, Book::all());
}

...并观察测试通过。非常不错,对吧?

魔法绑定

如果您的模型关系方法名(即:User模型上的customers()方法)与我们的Factory类(即:CustomerFactory)相同或为复数形式,那么我们可以利用Poser中的魔法绑定。

让我们再次看看我们的User/Customer示例。

/** @test */
public function user_has_customers()
{
    $user = UserFactory::new()
        ->withCustomers(CustomerFactory::times(30))
        ->create();

    $this->assertCount(30, $user->customers);
}

Poser足够智能,能够推断出withCustomers()是对CustomerFactory的引用,并允许我们像这样重写测试

/** @test */
public function user_has_customers()
{
    $user = UserFactory::new()
        ->withCustomers(30)
        ->create();

    $this->assertCount(30, $user->customers);
}

传递给withCustomers()的第一个参数是我们想要创建的客户数量,在本例中为:30

想象一下,为了一个牵强的例子,每个客户都应该叫“Joe Bloggs”。我们可以向withCustomers()传递一个关联数组作为第二个参数,定义列名和值,就像我们在createmakewithAttributes方法中所做的那样

/** @test */
public function user_has_customers()
{
    $user = UserFactory::new()
        ->withCustomers(30, [
            "name" => "Joe Bloggs"
        ])
        ->create();

    $this->assertCount(30, $user->customers);
}

更进一步,想象一下,我们想要10个客户叫“Joe Bloggs”,10个叫“Jane Bloggs”,10个叫“John Doe”。Poser允许向withAttributesmakecreate方法以及任何使用魔法绑定的方法传递多个属性数组,以方便这种情况

/** @test */
public function user_has_customers()
{
    $user = UserFactory::new()
        ->withCustomers(30, ["name" => "Joe Bloggs"], ["name" => "Jane Bloggs"], ["name" => "John Doe"])
        ->create();

    $this->assertCount(10, $user->customers->filter(fn($customer) => $customer->name == "Joe Bloggs"));
    $this->assertCount(10, $user->customers->filter(fn($customer) => $customer->name == "Jane Bloggs"));
    $this->assertCount(10, $user->customers->filter(fn($customer) => $customer->name == "John Doe"));
}

对于像我们的User的地址这样的HasOne关系,我们可以做得很像

/** @test */
public function user_has_address()
{
    $user = UserFactory::new()
        ->withAddress()
        ->create();

    $this->assertNotEmpty($user->address);
}

我们也可以传递一个属性数组,但在这个例子中,我们将其作为第一个参数传递

/** @test */
public function user_has_address()
{
    $user = UserFactory::new()
        ->withAddress([
            "line_1" => "1 Test Street" 
        ])
        ->create();

    $this->assertNotEmpty($user->address);
}

有时,with[RelationshipMethodName]可能不是最易读的选择。Poser还支持has[RelationshipMethodName]语法,如下所示

/** @test */
public function user_has_address()
{
    $user = UserFactory::new()
        ->hasAddress([
            "line_1" => "1 Test Street" 
        ])
        ->create();

    $this->assertNotEmpty($user->address);
}

在大多数情况下,我们甚至不需要调用create,当我们在构建的模型或集合上尝试访问方法或属性时(在这种情况下,是$user属性),Poser会为我们调用它。

/** @test */
public function user_has_address()
{
    $user = UserFactory::new()
        ->hasAddress([
            "line_1" => "1 Test Street" 
        ]);

    $this->assertNotEmpty($user->address); // When we access the $address property, Poser automatically calls `create` for us.
}

现在,让我们将这些全部放在一起,并展示在Poser中构建世界是多么简单。想象一下,我们想要10个用户,每个用户都有一个地址和20个客户。每个客户都应该有5本书。这应该是10个User、10个Address、200个Customer和1000本Book。看看吧

/** @test */
public function users_with_addresses_can_have_customers_with_books() {
    UserFactory::times(10)
               ->hasAddress()
               ->withCustomers(CustomerFactory::times(20)->withBooks(5))();

    $this->assertCount(1000, Book::all());
    $this->assertCount(200, Customer::all());
    $this->assertCount(10, User::all());
    $this->assertCount(10, Address::all());
}

让我们分析一下这段代码。首先,我们让UserFactory创建10个用户,并传递给它hasAddress()函数。Poser能够找到AddressFactory,因此它会自动为我们实例化它,并为每个用户提供一个Address

接下来,我们调用 withCustomers()。因为我们想要为每个 Customer 指定额外的参数,所以我们直接实例化 CustomerFactory,一次请求 20 个。然后我们将 withBooks() 连接到 CustomerFactory,简单地传递整数 5。Poser 会寻找 BookFactory,找到后,在后台自动调用 BookFactory::times(5)

最后,我们通过调用 UserFactory() 来完成语句。这是调用 UserFactory 上的 create() 的简写语法。因为我们在这项测试中是间接访问模型(我们在断言中从数据库请求模型,而不是访问用户属性或方法),所以我们必须调用 create() 方法。

为了参考,使用 Laravel 内置工厂的相同测试看起来是这样的

/** @test */
public function users_with_addresses_can_have_customers_with_books() {
    $user = factory(User::class)->times(10)->create();
    $user->each(function($user) {
        $user->address()->save(factory(Address::class)->make());
        $customers = factory(Customer::class)->times(20)->make();
        $user->customers()->saveMany($customers);
        $user->customers->each(function($customer) {
            $customer->books()->saveMany(factory(Book::class)->times(5)->make());
        });
    });

    $this->assertCount(1000, Book::all());
    $this->assertCount(200, Customer::all());
    $this->assertCount(10, User::all());
    $this->assertCount(10, Address::all());
}

多对多关系

Poser 支持使用与您现在熟悉完全相同的 with[RelationshipMethodName]()has[RelationshipMethodName]() 语法来支持多对多关系。让我们以一个常见的例子为例,一个可以拥有许多 RoleUser,以及一个可以拥有许多 UserRole

/** @test */
public function a_user_can_have_many_roles() {
    $user = UserFactory::new()->withRoles(3)();

    $this->assertCount(3, $user->roles);
}

/** @test */
public function a_role_can_have_many_users() {
    $role = RoleFactory::new()->hasUsers(5)();

    $this->assertCount(5, $role->users);
}

当使用 withPivotAttributes() 方法处理多对多关系时,Poser 允许您将数据保存到您的连接表中

/** @test */
public function a_user_can_have_many_roles() {
    $expiry = now();
    $user = UserFactory::new()->withRoles(RoleFactory::new()->withPivotAttributes([
        'expires_at' => $expiry
    ]))();

    $this->assertDatabaseHas('role_user', [
        'user_id' => $user->id,
        'expires_at' => $expiry
    ]);
}

对于希望在每个相关模型上保存不同连接数据的实例,withPivotAttributes() 接受多个属性集

/** @test */
public function a_user_can_have_many_roles() {
    $user = UserFactory::new()->withRoles(RoleFactory::times(3)->withPivotAttributes(
        ['expires_at' => now()->addDay()],
        ['expires_at' => now()->addDays(2)],
        ['expires_at' => now()->addDays(3)]
    ))();

    // ...assertions go here
}

请注意,在添加连接属性时,不要在关系工厂上使用 make()create()invoke() 方法,因为 Poser 在保存模型时将无法访问它们。

$user = UserFactory::new()->withRoles(RoleFactory::new()->withPivotAttributes([
    'expires_at' => $expiry
])->make())(); // Don't do this

$user = UserFactory::new()->withRoles(RoleFactory::new()->withPivotAttributes([
    'expires_at' => $expiry
]))(); // Do this instead

多态关系

Poser 支持使用与您现在非常熟悉的完全相同的 with[RelationshipMethodName]()has[RelationshipMethodName]() 语法来支持所有多态关系类型。想象一下,我们的 UserCustomer 模型都可以有 Comment。您的 Poser 测试可能看起来像这样

/** @test */
public function a_user_can_have_many_comments() {
    $user = UserFactory::new()->withComments(10)();

    $this->assertCount(10, $user->comments);
}

/** @test */
public function a_customer_can_have_many_comments() {
    $customer = CustomerFactory::new()->withComments(25)->forUser(UserFactory::new())();

    $this->assertCount(25, $customer->comments);
}

多对多多态关系的工作方式完全相同。

属于关系

如果我们想创建一个 BelongsTo 关系呢?Poser 也很容易做到这一点。我们不需要在前面加上 with,我们可以加上 for。让我们再看看我们的例子。假设我们想要请求一个客户被分配一个用户。只需这样做

/** @test */
public function customer_has_user()
{
    $customer = CustomerFactory::new()
        ->forUser(
            UserFactory::new()
        )->create();

    $this->assertNotEmpty($customer->user);
}

也不需要调用 UserFactory 上的 create;Poser 会为您做这件事!

静态实例化

如果您只需要一个模型的单个实例,您不一定必须使用 ::new() 方法。当静态调用时,Poser 将构建一个工厂实例并为您处理关系方法。所以,我们不必写 UserFactory::new()->withCustomers(5)->create(),我们可以改为写 UserFactory::withCustomers(5)->create()

请注意,静态解析仅适用于动态方法,不适用于您在工厂中定义的方法或 Poser 类中内置的方法。因此,UserFactory::withoutEvents()->create() 不会工作。

工厂状态

如果您在 Laravel 工厂中设置了任何状态,那么您也可以使用它们与 Poser 一起使用。所以如果您的 Laravel 客户端工厂类中有以下设置的状态

$factory->state(Customer::class, 'active', function (Faker $faker) {
    return [
        'active' => true,
    ];
});

$factory->state(Customer::class, 'inactive', function (Faker $faker) {
    return [
        'active' => false,
    ];
});

那么您可以使用 state 方法告诉 Poser 也使用这些工厂状态

/** @test */
public function customer_is_active()
{
    $customer = CustomerFactory::new()
        ->state('active')
        ->create();

    $this->assertTrue($customer->active);
}

您还可以使用 as 方法,这实际上调用 state 方法

/** @test */
public function customer_is_active()
{
    $customer = CustomerFactory::new()
        ->as('active')
        ->create();

    $this->assertTrue($customer->active);
}

类似于Laravel的工厂,也存在一个states方法,允许您使用多个状态

$customer = CustomerFactory::new()
    ->states('state1', 'state2', 'etc')
    ->create();

由于Poser利用了内置的Laravel工厂方法,您可以在您的Laravel数据库工厂中使用afterMaking()afterCreating()afterMakingState()afterCreatingState()回调,就像您以前做的那样,并且它们会按预期被调用。

创建后

类似于Laravel的模型工厂,Poser还提供了一个afterCreating()方法,该方法可以接受一个闭包,在记录创建后运行。

示例

  • 用户属于一个公司
  • 公司有一个主用户(存储在公司记录上)
    • 在Company上设置主用户是一个仅更新Company记录上的main_user_id列的函数
CompanyFactory::new()
    ->afterCreating(\App\Company $company) {
        $company->setMainUser(UserFactory::new()->forCompany($company)->create());
    })->create();

因此,在Company创建后,它将为该公司创建一个新的用户,并将其设置为该公司的主用户。

当使用times()创建多个公司时,这同样适用,并且将为每个创建的公司创建一个新用户

CompanyFactory::times(3)
    ->afterCreating(\App\Company $company) {
        $company->setMainUser(UserFactory::new()->forCompany($company)->create());
    })->create();

这样的逻辑可能会被广泛使用,这就是Poser按类分类的优势所在,因为您可以将这种逻辑存储在CompanyFactory类上的函数中,如下所示

class CompanyFactory extends Factory {
    public function withMainUser()
    {
        return $this->afterCreating(function(Company $company) {
            $company->setMainUser(UserFactory::new()->forCompany($company)->create());
        });
    }
}

允许您只调用withMainUser()

CompanyFactory::times(3)
    ->withMainUser()
    ->create();

afterCreating()方法也适用于嵌套关系

/** @test */

$user = UserFactory::new()->withCustomers(CustomerFactory::times(10)->afterCreating(function($customer, $user) {
    // Perform an action to the newly created (and linked) customer model
}))->create();

注意,嵌套关系也将父模型(在本例中为创建的User)作为闭包的第二个参数提供。

工厂API

::new()

创建工厂的新实例。如果您只想创建一个模型,请使用此方法实例化类。

如果您在这次调用后直接调用动态关系方法,您可以完全删除此调用,而是静态调用关系方法,如下所示:UserFactory::withCustomers(5)->create()

::times($count)

创建工厂的新实例,但通知工厂您将创建多个模型。当您希望在数据库中创建多个条目时,请使用此方法实例化类。

::craft(int $count, ...$attributes)

允许您创建多个模型,将它们持久化到数据库中,并在一个方法调用中返回结果集合。当您不需要复杂的关系映射时,这是一个非常有用的快捷方式。您可以传递要创建的模型数量以及应该分配给这些模型的属性。

::craft(...$attributes, int $count)

上述craft的替代语法,它与Laravel中内置的factory方法更接近。

::craft(array $attributes = null)

如果您只需要创建一个模型,可以省略计数。它将返回单个模型而不是集合。

->create(...$attributes)(...$attributes)

类似于Laravel工厂的create命令,这将创建模型并将它们持久化到数据库。您可以传递一个关联数组,包含列名和期望的值,这些值将被应用于创建的模型。您可以可选地通过调用工厂来调用create,这允许使用更短的语法。

如果您在测试中直接与模型交互,而不是从数据库重新检索它们,您可以完全省略create调用。Poser会在您尝试访问属性或调用模型(/集合)的方法时自动调用create方法。

如果您希望为要创建的每个模型应用不同的属性,您可以传递任意数量的属性数组作为单独的参数。

->make(...$attributes)

类似于Laravel工厂的make命令,这将创建模型但不将它们持久化到数据库。您可以传递一个包含列名和期望的值的关联数组,这些值将被应用于创建的模型。

如果您希望为要创建的每个模型应用不同的属性,您可以传递任意数量的属性数组作为单独的参数。

->withAttributes(...$attributes)

您可以传递一个包含列名和期望的值的关联数组,这些值将被应用于创建的模型。

如果您希望为要创建的每个模型应用不同的属性,您可以传递任意数量的属性数组作为单独的参数。

->state(string $state)->as(string $state)

您可以传递在Laravel模型工厂中定义的工厂状态,这将应用于创建的模型。

->states(...$states)

类似于->state(string $state),但它允许您传递多个状态,这些状态都将应用于创建的模型。

->withPivotAttributes(...$attributes)

当处理多对多关系时,您可能希望在连接表上存储数据。您可以使用此方法来这样做,传递一个包含列名和期望的值的关联数组。这应在相关工厂上调用,而不是在根级工厂上。

如果您希望为要创建的每个模型应用不同的连接属性,您可以传递任意数量的属性数组作为单独的参数。

->afterCreating(Closure $closure)

允许您提供一个钩子,当给定的工厂创建了其模型时将被调用。这允许您对创建的模型执行额外的设置。它也可以放在关系以及父模型上。

闭包将创建的模型作为第一个参数提供。如果afterCreating()在关系上调用,它还将父模型作为第二个参数提供。

->withoutDefaults(...$defaultsToIgnore)

如果您希望忽略特定测试中的某些Poser默认值,可以将此方法链接到您的Poser工厂调用。如果您想忽略所有默认值,只需不带参数调用此方法即可。

$usersWithoutDefaultCustomers = UserFactory::new()->withoutDefaults('customers')->create();

->withoutEvents()

有时,在创建模型时禁用事件是有用的,以避免测试中的意外副作用。您可以将此方法附加到Poser工厂调用来确保在模型创建期间不会触发任何事件。

php artisan make:poser API

如果没有传递参数给命令,Poser将尝试为您应用中的每个模型创建匹配的工厂。它是通过查看您的poser.models_namespace配置条目,并扫描给定命名空间中的模型来实现的。您可以多次调用make:poser而不必担心它会覆盖您现有的工厂;如果它发现某个工厂已经存在,它将简单地跳过。

单独的工厂

您可以可选地传递一个名称给命令,该名称与您想要创建的工厂的名称相对应。例如,php artisan make:poser UserFactory将会在您的poser.factories_namespace配置文件中定义的命名空间中创建一个名为UserFactory的工厂。

-m--model标志

如果您的模型名称与您希望给工厂的名称不同,您可以传递一个-m--model标志,以及工厂将对应的模型名称。因此,php artisan make:poser ClientFactory -m Customer将创建一个名为ClientFactory的工厂,但指向Customer模型。

-f--factory标志

您可以将-f--factory传递给命令以可选地生成相应的Laravel数据库工厂。数据库工厂将采用[modelName]Factory的形式。

注意事项

模型位置

默认情况下,Poser将在App目录中查找您的模型,这对于大多数项目来说应该没问题。如果您将模型放在不同的目录中,您可以通过编辑poser.php配置文件中的models_namespace条目来让Poser知道。

如果您需要为单个实例覆盖模型位置,您可以在Factory类中覆盖$modelName静态变量,传入相应模型的完全限定类名。

工厂位置

默认情况下,Poser将在Tests/Factories目录中搜索您的Factory类。如果您将工厂放在不同的目录中(例如:Tests/Models/Factories),您可以通过编辑poser.php配置文件中的factories_namespace条目来让Poser知道。

嵌套模型

如果您有一个位于子文件夹中的模型怎么办?例如:app/Models/Customers/Customer.php。您可以通过复制模型的命名空间来生成此工厂:php artisan make:poser Models\\Customers\\CustomerFactory。注意,命名空间应该相对于poser.php配置文件中的models_namespace条目。

->create()->make()命令

您应该在最外层的Factory语句结束时调用create命令,使其持久化到数据库。您不需要在嵌套的Factory语句上调用createmake,因为Poser会为您做这件事。

唯一的例外是BelongsTo关系,在这种情况下,您必须在嵌套的Factory语句上调用create

如果你喜欢简洁的语法,可以将 ->create() 替换为 (),因为工厂的 __invoke 函数实际上只是调用了 create()

public function user_has_customers()
{
    $user = UserFactory::new()
        ->withCustomers(
            CustomerFactory::times(30)->withBooks(BookFactory::times(5))
        )();

    $this->assertCount(30, $user->customers);
    $this->assertCount(150, Book::all());
}

多个属性

在处理多个模型时,能够提供多个属性通常非常有用。Poser 在这方面非常智能,允许你无需任何努力就能进行一些非常强大的世界观构建。想象一下,我们想要创建3个具有不同名字的用户。没问题

$users = UserFactory::times(3)->create(["name" => "Joe"], ["name" => "Jane"], ["name" => "Jimmy"]);

你可以使用 createmakewithAttributes 以及像 withCustomers 这样的魔法绑定来完成这项工作。后者看起来是这样的

UserFactory::new()->withCustomers(3, ['name' => "Joe"], ["name" => "Jane"], ["name" => "Jimmy"])();

如果你提供的属性组比模型少,Poser 会为你循环属性组。所以,在下面的例子中...

$users = UserFactory::times(10)->create(["name" => "Joe"], ["name" => "Jane"]);

...Poser 将创建5个名叫 Joe 的用户和5个名叫 Jane 的用户。

默认关系

使用 Poser 一段时间后,你可能希望提供默认关系,以便每次创建模型时都应用。例如,在我们的例子中,一个 Customer 需要一个相关的 User 模型。为了便于操作,Poser 理解 Poser 工厂内部 default[RelationshipType][RelationshipMethodName] 语法。为了说明

class CustomerFactory extends Factory
{

    public function defaultForUser()
    {
        return UserFactory::new()->withAttributes(["name" => "Joe Bloggs"]);
    }

}

现在,每次我们调用...

CustomerFactory::new()->create();

...一个名叫 Joe BloggsUser 将自动分配给客户。然而,如果我们调用...

CustomerFactory::new()->forUser(UserFactory::new()->withAttributes(["name" => "John Doe"]))->create();

...默认值将被忽略,取而代之的是将一个名叫 "John Doe" 的 User 分配给 Customer

对于 with/has 关系类型,情况大致相同

class UserFactory extends Factory
{

    public function defaultWithAddress() 
    {
        return AddressFactory::new();
    }

    public function defaultHasCustomers()
    {
        return CustomerFactory::times(10);
    }

}

在这种情况下,当我们调用 UserFactory::new()->create() 时,它将获得一个 Address 和 10 个 Customer

如果我们想忽略特定测试的默认值,只需将 withoutDefaults 方法链式调用到你的 Factory 调用。

UserFactory::new()->withoutDefaults()->create();

这将创建一个没有任何 AddressCustomerUser。如果你只想忽略某些默认值,你可以将它们作为属性传递给 withoutDefaults 方法。

UserFactory::new()->withoutDefaults('customers')->create();

现在,我们创建的 User 将没有 Customer,但将有一个默认的 Address

内联测试

你经常会发现自己编写简短的测试来检查模型上的某些属性是否可以成功持久化到数据库,或者期望的模型数量是否与父模型相关。

Poser 为你提供了一个超级简单的语法来检查所有这些。它只需要你使用 PhpUnit 进行测试(这是 Laravel 中的默认测试库)。

只需在任何 create 函数之前将 PhpUnit 断言作为 Factory 的链式方法调用,然后看看魔法如何发生。

让我们想象一下,我们想要检查一个用户是否有 5 个附加的客户。使用内联测试,我们可以这样编写这个测试

/** @test */
public function a_user_can_have_customers()
{
    UserFactory::withCustomers(5)->assertCount(5, 'customers')();
}

仅用一行代码,我们就能断言创建的 User 模型上的 customers 属性的计数为 5。这里还有一个例子

/** @test */
public function a_user_can_have_customers()
{
    UserFactory::assertEquals('John Doe', 'name')(['name' => 'John Doe']);
}

当然,有时你需要更多的功能。为了便于操作,Poser 允许你将闭包作为所有断言方法的第二个参数传递

/** @test */
public function a_user_can_have_customers()
{
    UserFactory::assertEquals('John', fn($user) => $user->locateFirstName())(['name' => 'John Doe']);
}

如果你正在处理多个模型,你的方法将传递一个 Collection,如下所示

/** @test */
public function a_user_can_have_customers()
{
    UserFactory::times(5)->assertCount(5, fn($users) => $users)();
}

如果你想对 Collection 中的每个模型执行断言,你可以对闭包参数进行相关模型类型的类型提示。Poser 会为你迭代模型并运行断言

/** @test */
public function a_user_can_have_customers()
{
    UserFactory::times(5)->withCustomers(10)->assertCount(10, fn(User $user) => $user->customers)();
}

尽管我们不推荐在复杂用例中使用内联测试,但它对简单测试或主要断言在模型本身上的测试非常适用。

故障排除

当使用魔法绑定时,我得到一个 ArgumentsNotSatisfiableException

当Poser找不到满足请求的关系方法调用的工厂时,会抛出此错误。所以,想象你调用了 UserFactory::new()->withCustomers(10)();,但没有任何 CustomerFactory,Poser会抛出此错误。解决方案是创建工厂。在这种情况下,我们可以从终端调用 php artisan make:poser CustomerFactory 以自动为我们创建工厂。

此错误出现的另一种情况是,如果你的父模型的关系方法名称与子模型名称不同。为了说明,想象我们有一个为 User 模型创建的 UserFactory,它有一个 clients() 方法。该方法返回一个对 Customer 模型的多对一关系,并且你有一个Poser的 CustomerFactory

当我们调用 UserFactory::new()->withClients()() 时,Poser理解你正在使用 User 模型上的 clients() 方法,但它找不到相应的 ClientFactory(因为它实际上称为 CustomerFactory)。解决这个问题的方法是求助于标准绑定。因此,我们的更新调用将是

UserFactory::new()->withClients(CustomerFactory::times(10))();

当处理关系时,Poser创建的模型比预期多

这发生在你在Laravel工厂中声明默认闭包时。Poser无法检测到这些存在,因此在Poser创建模型时它们仍然会被创建。Poser在Poser默认值中为此提供了合适的解决方案。如果此问题影响您,我们建议从您的Laravel工厂中删除闭包,并在相关的Poser工厂中用默认值替换它们。让我们看一个例子。

$factory->define(Customer::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'user_id' => function() {
            return factory(User::class)->create()->id;
        }
    ];
});

这个 Customer Laravel工厂有一个用于 user_id 的闭包,这在调用 UserFactory::new()->withCustomers(n)->create() 时会导致创建 n+1 个 User 模型。为了解决此问题,按如下方式更改上述工厂

$factory->define(Customer::class, function (Faker $faker) {
    return [
        'name' => $faker->name
    ];
});

现在,我们可以像这样更改Customer Poser工厂

class CustomerFactory extends Factory {

    public function defaultForUser()
    {
        return UserFactory::new();
    }

}

现在,当调用 UserFactory::new()->withCustomers(n)->create() 时,默认值将被忽略,因为我们已经设置了它。然而,当调用 CustomerFactory::new()->create() 时,将调用默认值,并为 Customer 设置一个默认的 User

更新日志

请查看 CHANGELOG.md 文件以了解每次更新的详细信息。

前后示例

向某人展示Poser的力量比解释它更容易,所以这里有一些前后代码片段。纯Laravel vs. Poser。请注意,以下所有示例在生成的工厂中都没有0代码,因此“幕后”没有进行额外的工作。

让我们从一个使用Poser的主要原因开始:具有复杂关系的测试。在这个例子中,一个用户可以有朋友。这些朋友是其他用户,并且他们通过查找表连接。在应用中,用户和他的朋友可以获得成就。在用户模型上,我们创建了一个自定义的friendsWithAllAchievements关系,该关系基于用户拥有所有可用的成就的事实。我们想要测试在调用时这个关系返回预期的朋友数量。

// Without Poser

/** @test */
function a_user_can_find_friends_with_all_achievements()
{
    $user = factory(User::class)->create();
    $achievements = factory(Achievement::class)->times(10)->create();

    $friendsWithAchievements = factory(User::class)->times(15)->create();
    $friendsWithAchievements->each(
        function ($friend) {
            $friend->achievements()->saveMany($achievements);
        }
    );

    $user->friends()->saveMany($friendsWithAchievements);

    $friendsWithoutAchievements = factory(User::class)->times(20)->create();

    $user->friends()->saveMany($friendsWithoutAchievements);

    $this->assertCount(15, $user->friendsWithAllAchievements);
}

// With Poser

/** @test */
function a_user_can_find_friends_with_all_achievements()
{
    $achievements = AchievementFactory::times(10)();

    UserFactory::withFriends(UserFactory::times(15)->withAchievements($achievements))
               ->withFriends(UserFactory::times(20))
               ->assertCount(15, 'friendsWithAllAchievements')();
}

正如你所见,我们用Poser编写的测试要简单得多,更易读,更易于管理。当涉及到大量前置条件的测试时,Poser表现得尤为出色。它可以大大简化每个人的生活,而且不需要额外的努力!

然而,即使在“简单”的测试中,Poser也能让你的生活变得非常简单。例如,你经常会想要创建一个测试来确保关系按预期工作,尤其是在TDD工作流程中。对于下一个测试,我们的用户可能有多个客户,但他也可能没有客户。我们想要测试这两种可能性。

// Without Poser

/** @test */
function a_user_may_have_customers()
{
    $userWithCustomers = factory(User::class)->create();
    $userWithCustomers->customers()->saveMany(factory(Customer::class)->times(30)->make());

    $userWithoutCustomers = factory(User::class)->create();

    $this->assertCount(30, $userWithCustomers->customers);
    $this->assertTrue($userWithoutCustomers->customers->isEmpty());
}

// With Poser

/** @test */
function a_user_may_have_customers()
{
    UserFactory::withCustomers(30)->assertCount(30, 'customers')();
    UserFactory::assertTrue(fn($user) => $user->customers->isEmpty())();
}

即使在这个非常简单的测试中,我们也能将代码行数减少一半以上,同时提高可读性。在我们Poser示例中使用的每个函数都充满了意义和易懂性。在第一个测试中,你的眼睛可能会因为代码而变得模糊。你必须强迫自己去阅读和理解它。在我们的第二个测试中,我们的眼睛会被代码吸引。我们可以在几秒钟内获得对其目的的洞察,因为它几乎就像两个句子。

致谢

在Twitter上关注我 @LukeDowning19 以获取更新!

此外,请星标仓库。这真的很有帮助!❤️

安全

如果你发现任何与安全相关的问题,请通过电子邮件 lukeraymonddowning@gmail.com 而不是使用问题跟踪器。

测试

composer install
composer test

许可

MIT许可证(MIT)。请参阅许可文件以获取更多信息。

贡献者 ✨

感谢这些可爱的人们 (emoji key)


Alan Holmes

💻

AndrewP

📖 ⚠️

veganista

🤔

Brad Roberts

🤔

Rhys Botfield

💻

MB

🤔

Solomon Antoine

📖

Patompong Savaengsuk

💻

mishbah

🤔

Dainius

💻

此项目遵循all-contributors规范。欢迎任何类型的贡献!