Фабрики моделей Laravel — одна из лучших функций, которые вы можете использовать в своем приложении, когда дело доходит до тестирования. Они позволяют определять предсказуемые и легко воспроизводимые данные, чтобы ваши тесты были согласованными и контролируемыми.
Начнем с простого примера. У нас есть приложение, используемое для ведения блога, поэтому, естественно, у нас есть Post
модель со статусом публикации, черновика или очереди. Давайте посмотрим на Eloquent Model для этого примера:
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',
];
}
Как вы можете видеть здесь, у нас есть Enum для столбца статуса, который мы сейчас разработаем. Использование здесь перечисления позволяет нам использовать возможности 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');
});
Отлично, мы можем передать аргумент в метод create, чтобы убедиться, что мы устанавливаем правильный тип, когда мы создаем его так, что наши бизнес-правила не будут жаловаться. Но это немного громоздко, чтобы постоянно писать, поэтому давайте немного рефакторим нашу фабрику, чтобы добавить методы для изменения состояния:
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),
],
);
}
}
Итак, в наших тестах мы можем создать новый тест, который гарантирует, что мы можем обновить черновик сообщения. 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();
);
});
Как вы используете фабрики моделей в своем приложении? Вы нашли какие-нибудь интересные способы их использования? Дайте нам знать в твиттере!