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();
);
});