• Czas czytania ~6 min
  • 10.08.2022

Fabryki modelu Laravel to jedna z najlepszych funkcji, których możesz użyć w swojej aplikacji, jeśli chodzi o testowanie. Zapewniają sposób definiowania danych, które są przewidywalne i łatwe do replikacji, aby Twoje testy były spójne i kontrolowane.

Zacznijmy od prostego przykładu. Mamy aplikację służącą do blogowania, więc oczywiście mamy Postmodel, który ma stan określający, czy post jest opublikowany, sporządzony lub umieszczony w kolejce. Spójrzmy na model elokwentny dla tego przykładu:

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',
	];
}

Jak widać tutaj, mamy Enum dla kolumny stanu, którą teraz zaprojektujemy. Użycie tutaj wyliczenia pozwala nam skorzystać z funkcji PHP 8.1 zamiast zwykłych ciągów, flag logicznych lub niechlujnych wyliczeń bazy danych.

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

Teraz wróćmy do tematu, który tu omawiamy: fabryki modeli. Prosta fabryka wyglądałaby bardzo prosto:

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,
		];
	}
}

W naszych testach możemy teraz szybko zadzwonić do naszej fabryki pocztowej, aby utworzyć dla nas post. Zobaczmy, jak możemy to zrobić:

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

Wystarczająco prosty test, ale co się stanie, jeśli mamy reguły biznesowe, które mówią, że można aktualizować tylko określone kolumny w zależności od typ posta?Zrefaktoryzujmy nasz test, aby upewnić się, że możemy to zrobić:

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

Idealnie, możemy przekazać argument do metody create, aby upewnić się, że ustawiamy właściwy typ, gdy tworzymy go tak, aby nasze zasady biznesowe nie narzekały. Ale to jest trochę kłopotliwe, aby dalej pisać, więc zrefaktoryzujmy trochę naszą fabrykę, aby dodać metody modyfikacji stanu:

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

Ustawiamy wartość domyślną dla naszej fabryki, aby wszystkie nowo tworzone posty były szkicami. Następnie dodajemy metodę ustawienia stanu do opublikowania, która użyje prawidłowej wartości Enum i ustawi datę publikacji - o wiele bardziej przewidywalną i powtarzalną w środowisku testowym. Zobaczmy, jak teraz będzie wyglądał nasz test:

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

Wracając do bycia prostym testem - więc jeśli mamy wiele testów, które chcą stworzyć wersję roboczą posta, mogą użyć fabryki. Teraz napiszemy test dla opublikowanego stanu i zobaczmy, czy otrzymamy błąd.

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

Tym razem testujemy, czy otrzymujemy stan błędu walidacji, gdy próbujemy aby zaktualizować opublikowany post.Gwarantuje to, że chronimy naszą zawartość i wymuszamy określony przepływ pracy w naszej aplikacji.

Co się stanie, jeśli chcemy również zapewnić określoną zawartość w naszej fabryce? Możemy dodać inną metodę, aby zmodyfikować stan, zgodnie z potrzebami:

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),
			],
		);
	}
}

W naszych testach możemy więc utworzyć nowy test, który zapewni, że możemy zaktualizować wersję roboczą postów tytuł za pośrednictwem naszego interfejsu 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');
});

Dzięki temu możemy sprawnie kontrolować rzeczy w naszym środowisku testowym, używając stanów fabrycznych, dając nam tyle kontroli, ile potrzebujemy. Spowoduje to, że będziemy konsekwentnie przygotowywać nasze testy lub będzie dobrym odzwierciedleniem stanu aplikacji w określonych punktach.

Co robimy, jeśli musimy tworzyć wiele modeli do naszych testów? Jak możemy to zrobić? Prostą odpowiedzią byłoby powiedzenie fabryce:

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

Więc tworzymy 12 nowych postów i upewniamy się, że kiedy otrzymamy trasę indeksu, będziemy zwracać 12 postów. Zamiast przekazywać licznik do metody fabrycznej, możesz również użyć metody count:

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

Jednak zdarzają się sytuacje w naszej aplikacji, kiedy możemy chcieć uruchomić coś w konkretne zamówienie. Powiedzmy, że chcemy, aby pierwszy był szkicem, ale drugi został opublikowany?

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

Jak korzystasz z fabryk modeli w swojej aplikacji? Czy znalazłeś jakieś fajne sposoby na ich wykorzystanie? Daj nam znać na Twitterze!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

O

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...

O autorze CrazyBoy49z
WORK EXPERIENCE
Kontakt
Ukraine, Lutsk
+380979856297