如何在你的测试中使用Laravel模型工厂

133 阅读3分钟

Laravel模型工厂是一个最好的功能,你可以在你的应用程序中使用,当它涉及到测试。他们提供了一种方法来定义数据,是可预测的,容易复制的,所以你的测试是一致的,可控的。

让我们从一个简单的例子开始。我们有一个用于博客的应用程序,所以很自然地,我们有一个Post 模型,该模型有一个状态,如果帖子被发布,起草,或排队。让我们看一下这个例子的Eloquent模型。

declare(strict_types=1);
 
namespace App\Models;
 
use App\Publishing\Enums\PostStatus;
use Illuminate\Database\Model;
 
class Post extends Model
{
    protected $fillable = [
        'title',
        'slug',
        'content',
        'status',
        'published_at',
    ];
 
    protected $casts = [
        'status' => PostStatus::class,
        'published_at' => 'datetime',
    ];
}

正如你在这里看到的,我们有一个状态列的枚举,我们现在要设计它。在这里使用一个枚举允许我们利用PHP 8.1的特性,而不是普通的字符串、布尔标志或混乱的数据库枚举。

declare(strict_types=1);
 
namespace App\Publishing\Enums;
 
enum PostStatus: string
{
    case PUBLISHED = 'published';
    case DRAFT = 'draft';
    case QUEUED = 'queued';
}

现在,让我们回到我们要讨论的主题:模型工厂。一个简单的工厂看起来会非常简单。

declare(strict_types=1);
 
namespace Database\Factories;
 
use App\Models\Post;
use App\Publishing\Enums\PostStatus;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
 
class PostFactory extends Factory
{
    protected $model = Post::class;
 
    public function definition(): array
    {
        $title = $this->faker->sentence();
        $status = Arr::random(PostStatus::cases());
 
        return [
            'title' => $title,
            'slug' => Str::slug($title),
            'content' => $this->faker->paragraph(),
            'status' => $status->value,
            'published_at' => $status === PostStatus::PUBLISHED
                ? now()
                : null,
        ];
    }
}

所以在我们的测试中,我们现在可以快速调用我们的帖子工厂来为我们创建一个帖子。让我们来看看我们如何做到这一点。

it('can update a post', function () {
    $post = Post::factory()->create();
 
    putJson(
        route('api.posts.update', $post->slug),
        ['content' => 'test content',
    )->assertSuccessful();
 
    expect(
        $post->refresh()
    )->content->toEqual('test content');
});

一个足够简单的测试,但如果我们有业务规则,说你只能根据帖子类型更新特定的列,会发生什么?让我们重构我们的测试以确保我们能做到这一点。

it('can update a post', function () {
    $post = Post::factory()->create([
        'type' => PostStatus::DRAFT->value,
    ]);
 
    putJson(
        route('api.posts.update', $post->slug),
        ['content' => 'test content',
    )->assertSuccessful();
 
    expect(
        $post->refresh()
    )->content->toEqual('test content');
});

完美,我们可以在创建方法中传递一个参数,以确保我们在创建时设置正确的类型,这样我们的业务规则就不会抱怨。但这样一直写下去就有点麻烦了,所以让我们对工厂进行一下重构,增加修改状态的方法。

declare(strict_types=1);
 
namespace Database\Factories;
 
use App\Models\Post;
use App\Publishing\Enums\PostStatus;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
 
class PostFactory extends Factory
{
    protected $model = Post::class;
 
    public function definition(): array
    {
        $title = $this->faker->sentence();
 
        return [
            'title' => $title,
            'slug' => Str::slug($title),
            'content' => $this->faker->paragraph(),
            'status' => PostStatus::DRAFT->value,
            'published_at' => null,
        ];
    }
 
    public function published(): static
    {
        return $this->state(
            fn (array $attributes): array => [
                'status' => PostStatus::PUBLISHED->value,
                'published_at' => now(),
            ],
        );
    }
}

我们为我们的工厂设置一个默认值,使所有新创建的帖子都是草稿。然后我们添加一个方法来设置状态为发布,这个方法将使用正确的Enum值并设置发布日期--在测试环境中更具有可预测性和可重复性。让我们看看我们的测试现在会是什么样子。

it('can update a post', function () {
    $post = Post::factory()->create();
 
    putJson(
        route('api.posts.update', $post->slug),
        ['content' => 'test content',
    )->assertSuccessful();
 
    expect(
        $post->refresh()
    )->content->toEqual('test content');
});

回到一个简单的测试--所以如果我们有多个测试想创建一个草稿文章,他们可以使用工厂。现在让我们为发布状态写一个测试,看看我们是否得到一个错误。

it('returns an error when trying to update a published post', function () {
    $post = Post::factory()->published()->create();
 
    putJson(
        route('api.posts.update', $post->slug),
        ['content' => 'test content',
    )->assertStatus(Http::UNPROCESSABLE_ENTITY());
 
    expect(
        $post->refresh()
    )->content->toEqual($post->content);
});

这一次我们要测试的是,当我们试图更新一个已发布的帖子时,我们会收到一个验证错误的状态。这可以确保我们保护我们的内容,并在我们的应用程序中强制执行一个特定的工作流程。

那么,如果我们也想在我们的工厂中确保特定的内容会怎样呢?我们可以添加另一个方法,根据我们的需要修改状态。

declare(strict_types=1);
 
namespace Database\Factories;
 
use App\Models\Post;
use App\Publishing\Enums\PostStatus;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
 
class PostFactory extends Factory
{
    protected $model = Post::class;
 
    public function definition(): array
    {
        return [
            'title' => $title = $this->faker->sentence(),
            'slug' => Str::slug($title),
            'content' => $this->faker->paragraph(),
            'status' => PostStatus::DRAFT->value,
            'published_at' => null,
        ];
    }
 
    public function published(): static
    {
        return $this->state(
            fn (array $attributes): array => [
                'status' => PostStatus::PUBLISHED->value,
                'published_at' => now(),
            ],
        );
    }
 
    public function title(string $title): static
    {
        return $this->state(
            fn (array $attributes): array => [
                'title' => $title,
                'slug' => Str::slug($title),
            ],
        );
    }
}

因此,在我们的测试中,我们可以创建一个新的测试,确保我们可以通过我们的API更新一个草稿文章的标题。

it('can update a draft posts title', function () {
    $post = Post::factory()->title('test')->create();
 
    putJson(
        route('api.posts.update', $post->slug),
        ['title' => 'new title',
    )->assertSuccessful();
 
    expect(
        $post->refresh()
    )->title->toEqual('new title')->slug->toEqual('new-title');
});

因此,我们可以在测试环境中很好地使用工厂状态来控制事情,给我们提供我们需要的控制。这样做可以确保我们的测试准备工作始终如一,或者会很好地反映出应用程序在特定点的状态。

如果我们需要为我们的测试创建许多模型,我们该怎么做?我们怎样才能做到这一点呢?简单的答案是告诉工厂。

it('lists all posts', function () {
    Post::factory(12)->create();
 
    getJson(
        route('api.posts.index'),
    )->assertOk()->assertJson(fn (AssertableJson $json) =>
        $json->has(12)->etc(),
    );
});

所以我们要创建12个新帖子,并确保当我们得到索引路由时,有12个帖子返回。你也可以使用count方法,而不是将计数传入工厂方法。

Post::factory()->count(12)->create();

然而,在我们的应用程序中,有些时候我们可能希望以特定的顺序运行事情。比方说,我们希望第一篇是草稿,但第二篇是发表的?

it('shows the correct status for the posts', function () {
    Post::factory()
        ->count(2)
        ->state(new Sequence(
            ['status' => PostStatus::DRAFT->value],
            ['status' => PostStatus::PUBLISHED->value],
        ))->create();
 
    getJson(
        route('api.posts.index'),
    )->assertOk()->assertJson(fn (AssertableJson $json) =>
        $json->where('id', 1)
            ->where('status' PostStatus::DRAFT->value)
            ->etc();
    )->assertJson(fn (AssertableJson $json) =>
        $json->where('id', 2)
            ->where('status' PostStatus::PUBLISHED->value)
            ->etc();
    );
});