• Время чтения ~4 мин
  • 10.08.2022

Фабрики моделей 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();
	);
});

Как вы используете фабрики моделей в своем приложении? Вы нашли какие-нибудь интересные способы их использования? Дайте нам знать в твиттере!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Об авторе CrazyBoy49z
WORK EXPERIENCE
Контакты
Ukraine, Lutsk
+380979856297