15%

Zaoszczędź 15% na wszystkich usługach hostingowych

Sprawdź swoje umiejętności i zdobądź Rabat na dowolny plan hostingowy

Użyj kodu:

Skills
Rozpocznij
09.10.2024

Fabryki Laravel: Tworzenie Realistycznych Danych Testowych za Pomocą Modeli Fabryki Laravel

Podczas tworzenia aplikacji w Laravel jednym z najczęstszych wąskich gardeł w procesie testowania jest generowanie sensownych, realistycznych danych. Fabryki Laravel to klasy definiujące schemat tworzenia instancji modeli Eloquent, wykorzystujące bibliotekę PHP Faker do generowania losowych, ale strukturalnie poprawnych wartości atrybutów — umożliwiając programistom wypełnianie baz danych i pisanie izolowanych testów bez ręcznego konstruowania danych testowych.

W przeciwieństwie do statycznych plików SQL z danymi początkowymi lub zakodowanych na stałe tablic, fabryki są kompozycyjne, stanowe i świadome relacji. Integrują się bezpośrednio z zestawami testów PHPUnit i Pest, obsługują leniwe wartościowanie atrybutów i skalują się od pojedynczej instancji modelu do tysięcy rekordów w jednym łańcuchu metod. Jeśli uruchamiasz Laravel w środowisku Hostingu VPS, fabryki stają się szczególnie cenne podczas uruchomień potoku CI/CD, resetowania środowisk stagingowych i scenariuszy testów obciążeniowych, gdzie powtarzalne, kontrolowane generowanie danych jest niezbędne.

Czym są fabryki Laravel i dlaczego mają znaczenie

Fabryki Laravel zostały gruntownie przeprojektowane w Laravel 8. Starsze podejście oparte na domknięciach `$factory->define()` zostało zastąpione dedykowanymi klasami PHP rozszerzającymi `IlluminateDatabaseEloquentFactoriesFactory`. Ta zmiana architektoniczna wprowadziła bezpieczeństwo typów, autouzupełnianie w IDE i czystsze oddzielenie logiki fabryki od definicji modeli.

Każda klasa fabryki implementuje metodę `definition()`, która zwraca tablicę asocjacyjną atrybutów modelu. Fabryka automatycznie rozwiązuje instancję `FakerGenerator`, dostępną przez `$this->faker`, która obsługuje ponad 200 dostawców danych uwzględniających lokalizację — od `name()` i `safeEmail()` po `iban()`, `latitude()`, `uuid()` i `creditCardNumber()`.

Kluczowe możliwości nowoczesnego systemu fabryk:

  • Płynne łańcuchowanie metod do konfiguracji liczby, stanu i relacji
  • Leniwe wartościowanie atrybutów — domknięcia wewnątrz `definition()` są wartościowane od nowa dla każdej instancji
  • Stany fabryki do modelowania wariantów specyficznych dla domeny (np. zawieszone konta, zweryfikowani użytkownicy)
  • Fabryki relacji rekurencyjnie tworzące modele nadrzędne, gdy jest to potrzebne
  • Sekwencje do cyklicznego przechodzenia przez predefiniowane zestawy atrybutów
  • `make()` vs `create()` dla instancji w pamięci vs utrwalonych

Wymagania wstępne

Przed implementacją fabryk upewnij się, że Twoje środowisko spełnia następujące wymagania:

  • Laravel 9 lub nowszy (Laravel 8 jest kompatybilny, ale brakuje mu niektórych nowszych funkcji sekwencji)
  • PHP 8.0 lub wyższy
  • Skonfigurowane połączenie z bazą danych w `.env` (MySQL, PostgreSQL lub SQLite do testowania w pamięci)
  • Pakiet `laravel/framework`, który jest dostarczany z `fakerphp/faker` jako zależność
  • Modele Eloquent z odpowiadającymi plikami migracji

Dla zespołów uruchamiających Laravel na zarządzanej infrastrukturze, VPS z cPanel zapewnia wygodne środowisko do zarządzania zarówno stosem aplikacji, jak i usługami bazy danych z jednego interfejsu.

Krok 1: Generowanie klasy fabryki

Użyj Artisan CLI, aby wygenerować plik fabryki:

“`bash

php artisan make:factory UserFactory

“`

Tworzy to `database/factories/UserFactory.php`. Jeśli chcesz automatycznie powiązać fabrykę z modelem, przekaż flagę `–model`:

“`bash

php artisan make:factory UserFactory –model=User

“`

Laravel rozwiązuje powiązanie fabryki z modelem poprzez konwencję nazewnictwa: `UserFactory` mapuje do `AppModelsUser`. Możesz to nadpisać, ustawiając właściwość `protected $model` explicite, co jest niezbędne, gdy Twoje modele znajdują się poza domyślną przestrzenią nazw `AppModels`.

Krok 2: Definiowanie schematu fabryki

Otwórz `database/factories/UserFactory.php` i zdefiniuj metodę `definition()`:

“`php

<?php

namespace DatabaseFactories;

use AppModelsUser;

use IlluminateDatabaseEloquentFactoriesFactory;

use IlluminateSupportStr;

use IlluminateSupportFacadesHash;

class UserFactory extends Factory

{

protected $model = User::class;

public function definition(): array

{

return [

'name' => $this->faker->name(),

'email' => $this->faker->unique()->safeEmail(),

'email_verified_at' => now(),

'password' => Hash::make('password'),

'remember_token' => Str::random(10),

];

}

}

“`

Uwagi na poziomie atrybutów:

  • `$this->faker->unique()->safeEmail()` — modyfikator `unique()` utrzymuje rejestr unikalności per żądanie. Jeśli wyczerpiesz dostępne unikalne wartości (rzadkie, ale możliwe przy bardzo dużych zbiorach danych), Faker rzuca `OverflowException`. Zresetuj go za pomocą `$this->faker->unique(true)`, aby wyczyścić pamięć podręczną.
  • `Hash::make('password')` jest preferowane nad bezpośrednim `bcrypt()`, ponieważ respektuje skonfigurowany sterownik haszowania aplikacji (bcrypt, argon2i, argon2id).
  • `email_verified_at => now()` oznacza użytkownika jako już zweryfikowanego. Pomiń to pole lub ustaw je na `null`, aby zasymulować niezweryfikowane konto — częsty wariant stanu.

Krok 3: Tworzenie instancji modeli

3.1 Utrwalenie pojedynczego rekordu

“`php

$user = AppModelsUser::factory()->create();

“`

Wykonuje to instrukcję `INSERT` i zwraca uwodnioną instancję Eloquent `User`. Zwrócona instancja odzwierciedla rzeczywisty stan bazy danych, w tym wszelkie domyślne wartości na poziomie bazy danych lub wyzwalacze.

3.2 Utrwalenie wielu rekordów

“`php

$users = AppModelsUser::factory()->count(10)->create();

“`

Zwraca `IlluminateDatabaseEloquentCollection` 10 instancji `User`. Każdy rekord otrzymuje niezależnie wygenerowane wartości Faker — nie są to kopie pojedynczej instancji.

3.3 Instancja w pamięci bez utrwalania

“`php

$user = AppModelsUser::factory()->make();

“`

Metoda `make()` tworzy instancję modelu i wypełnia jego atrybuty bez dotykania bazy danych. Jest to idealne rozwiązanie dla testów jednostkowych weryfikujących zachowanie modelu, rzutowanie atrybutów lub logikę akcesorów/mutatorów w izolacji — utrzymując testy szybkimi i niezależnymi od bazy danych.

3.4 Nadpisywanie konkretnych atrybutów

Zarówno `create()`, jak i `make()` akceptują tablicę nadpisań atrybutów:

“`php

$user = AppModelsUser::factory()->create([

'email' => 'specific@example.com',

'name' => 'Jane Doe',

]);

“`

Nadpisania mają pierwszeństwo przed wartościami `definition()`. Jest to właściwy wzorzec, gdy test zależy od konkretnej, znanej wartości atrybutu, a nie losowej.

Krok 4: Stany fabryki

Stany to nazwane modyfikacje bazowej definicji fabryki. Pozwalają modelować odrębne warunki domenowe bez duplikowania całej fabryki.

4.1 Definiowanie stanów

“`php

public function unverified(): static

{

return $this->state(fn (array $attributes) => [

'email_verified_at' => null,

]);

}

public function admin(): static

{

return $this->state(fn (array $attributes) => [

'is_admin' => true,

'role' => 'administrator',

]);

}

public function suspended(): static

{

return $this->state(fn (array $attributes) => [

'suspended_at' => now(),

'is_active' => false,

]);

}

“`

4.2 Stosowanie stanów

“`php

// Single state

$adminUser = AppModelsUser::factory()->admin()->create();

// Stacked states — fully composable

$suspendedAdmin = AppModelsUser::factory()->admin()->suspended()->create();

“`

Stany są wartościowane w kolejności ich łańcuchowania. Późniejsze stany nadpisują konfliktujące klucze z wcześniejszych, zapewniając przewidywalne, warstwowe rozwiązywanie atrybutów.

Krok 5: Sekwencje do cyklicznego przechodzenia przez wartości atrybutów

Gdy potrzebujesz naprzemiennie używać zdefiniowanego zestawu wartości zamiast losowych, użyj `Sequence`:

“`php

use IlluminateDatabaseEloquentFactoriesSequence;

$users = AppModelsUser::factory()

->count(6)

->state(new Sequence(

['role' => 'editor'],

['role' => 'viewer'],

['role' => 'moderator'],

))

->create();

“`

Cyklicznie przechodzi przez tablicę sekwencji, przypisując role po kolei. Przy 6 użytkownikach każda rola jest przypisana dwukrotnie. Sekwencje są nieocenione przy testowaniu paginacji, kontroli dostępu opartej na rolach i logiki renderowania UI zależnej od zróżnicowanych, ale kontrolowanych rozkładów danych.

Krok 6: Fabryki relacji

6.1 Definiowanie relacji Belongs-To

W `PostFactory.php` odwołaj się bezpośrednio do fabryki nadrzędnej jako wartości atrybutu:

“`php

<?php

namespace DatabaseFactories;

use AppModelsPost;

use AppModelsUser;

use IlluminateDatabaseEloquentFactoriesFactory;

class PostFactory extends Factory

{

protected $model = Post::class;

public function definition(): array

{

return [

'user_id' => User::factory(),

'title' => $this->faker->sentence(),

'body' => $this->faker->paragraphs(3, true),

'slug' => $this->faker->unique()->slug(),

];

}

}

“`

Gdy `user_id` jest ustawione na `User::factory()`, Laravel odkłada jego wartościowanie. Jeśli wywołasz `Post::factory()->create()` bez podania `user_id`, nowy `User` jest automatycznie tworzony i używany jest jego klucz główny. Jeśli podasz istniejącego użytkownika, zagnieżdżona fabryka jest całkowicie pomijana.

6.2 Dołączanie do istniejącego rodzica

“`php

$user = AppModelsUser::factory()->create();

$posts = AppModelsPost::factory()->count(5)->for($user)->create();

“`

Metoda `for()` ustawia klucz obcy `user_id` na klucz główny dostarczonego modelu, zapobiegając niepotrzebnemu tworzeniu użytkownika. Jest to właściwy wzorzec, gdy Twój test ma już konkretnego użytkownika w zakresie.

6.3 Relacje Has-Many

“`php

$userWithPosts = AppModelsUser::factory()

->has(AppModelsPost::factory()->count(3), 'posts')

->create();

“`

Lub używając magicznego skrótu `hasPosts()` (rozwiązanego przez nazwę metody relacji w modelu):

“`php

$userWithPosts = AppModelsUser::factory()->hasPosts(3)->create();

“`

Tworzy to jednego użytkownika i trzy powiązane posty w jednej, atomowej operacji — z prawidłowo rozwiązanymi wszystkimi kluczami obcymi.

6.4 Relacje Many-to-Many

“`php

$user = AppModelsUser::factory()

->hasAttached(

AppModelsRole::factory()->count(2),

['assigned_at' => now()]

)

->create();

“`

Metoda `hasAttached()` obsługuje wstawianie do tabeli przestawnej, w tym wszelkie dodatkowe atrybuty przestawne, które musisz wypełnić.

Krok 7: Używanie fabryk w testach

7.1 Test funkcjonalny z asercjami bazy danych

“`php

use IlluminateFoundationTestingRefreshDatabase;

class UserTest extends TestCase

{

use RefreshDatabase;

public function test_user_can_be_created_with_factory(): void

{

$user = AppModelsUser::factory()->create();

$this->assertDatabaseHas('users', [

'email' => $user->email,

]);

}

public function test_unverified_user_cannot_access_dashboard(): void

{

$user = AppModelsUser::factory()->unverified()->create();

$response = $this->actingAs($user)->get('/dashboard');

$response->assertRedirect('/email/verify');

}

}

“`

Kluczowy szczegół: Zawsze używaj cechy `RefreshDatabase` lub `DatabaseTransactions` w klasach testowych, które wchodzą w interakcję z bazą danych. `RefreshDatabase` uruchamia migracje od nowa przed zestawem testów i opakowuje każdy test w transakcję, która jest wycofywana po zakończeniu, utrzymując testy izolowane i idempotentne.

7.2 Test jednostkowy z `make()`

“`php

public function test_user_full_name_accessor(): void

{

$user = AppModelsUser::factory()->make([

'name' => 'Alice Wonderland',

]);

$this->assertEquals('Alice Wonderland', $user->name);

}

“`

Nie dochodzi do żadnej interakcji z bazą danych. Test działa w mikrosekundach i nadaje się do wysokoczęstotliwościowych potoków CI.

Krok 8: Seedery bazy danych z fabrykami

8.1 Tworzenie seedera

“`bash

php artisan make:seeder UserSeeder

“`

8.2 Implementacja seedera

“`php

<?php

namespace DatabaseSeeders;

use AppModelsUser;

use IlluminateDatabaseSeeder;

class UserSeeder extends Seeder

{

public function run(): void

{

User::factory()

->count(50)

->create();

}

}

“`

8.3 Złożony seeder z relacjami

“`php

<?php

namespace DatabaseSeeders;

use AppModelsUser;

use AppModelsPost;

use IlluminateDatabaseSeeder;

class DatabaseSeeder extends Seeder

{

public function run(): void

{

User::factory()

->count(20)

->has(Post::factory()->count(5), 'posts')

->create();

// Create 5 admin users with no posts

User::factory()->count(5)->admin()->create();

}

}

“`

8.4 Uruchamianie seedera

“`bash

Run a specific seeder

php artisan db:seed –class=UserSeeder

Run all seeders defined in DatabaseSeeder

php artisan db:seed

Migrate fresh and seed in one command (common in staging resets)

php artisan migrate:fresh –seed

“`

Polecenie `migrate:fresh –seed` jest standardowym przepływem pracy do resetowania stagingowej lub deweloperskiej bazy danych do znanego, wypełnionego stanu. Na Serwerach Dedykowanych ten wzorzec jest często używany przed cyklami QA, aby zapewnić czyste, odtwarzalne środowisko.

Zaawansowane wzorce i przypadki brzegowe

Leniwe atrybuty i wartości zależne

Wartości Faker wewnątrz `definition()` są ponownie wartościowane dla każdego wywołania fabryki. Jednak jeśli potrzebujesz, aby jeden atrybut zależał od innego, użyj domknięcia:

“`php

public function definition(): array

{

$firstName = $this->faker->firstName();

$lastName = $this->faker->lastName();

return [

'first_name' => $firstName,

'last_name' => $lastName,

'email' => strtolower("{$firstName}.{$lastName}@example.com"),

'username' => strtolower("{$firstName}{$lastName}") . $this->faker->numerify('###'),

];

}

“`

Zapewnia to, że `email` i `username` są wyprowadzane z tych samych wartości nazwy, tworząc wewnętrznie spójne rekordy.

Unikanie pułapki przepełnienia `unique()`

Podczas generowania dużych zbiorów danych (10 000+ rekordów w jednym wywołaniu fabryki), `$this->faker->unique()->safeEmail()` może wyczerpać pulę unikalności Fakera i rzucić `OverflowException`. Złagodź to, dołączając UUID lub znacznik czasu do wygenerowanej wartości:

“`php

'email' => $this->faker->safeEmail() . '.' . $this->faker->uuid() . '@test.com',

“`

Gwarantuje to unikalność na dużą skalę bez polegania na wewnętrznym rejestrze unikalności Fakera.

Wywołania zwrotne fabryki: `afterMaking` i `afterCreating`

Używaj wywołań zwrotnych do wykonywania logiki po utworzeniu, której nie można wyrazić jako prostego atrybutu:

“`php

public function configure(): static

{

return $this->afterCreating(function (User $user) {

$user->profile()->create([

'bio' => $this->faker->paragraph(),

'avatar' => $this->faker->imageUrl(200, 200, 'people'),

]);

});

}

“`

Metoda `configure()` jest wywoływana raz podczas tworzenia instancji fabryki. `afterCreating` uruchamia się po utrwaleniu modelu, co czyni ją odpowiednią do tworzenia powiązanych modeli wymagających klucza głównego rodzica.

Testowanie funkcjonalności e-mail

Gdy fabryki generują użytkowników z adresami e-mail, testy integracyjne weryfikujące wysyłanie wiadomości e-mail korzystają z dedykowanego środowiska Hostingu Poczty E-mail skonfigurowanego z sandboxowym serwerem SMTP, zapobiegając przypadkowemu dostarczaniu testowych wiadomości e-mail na prawdziwe adresy.

`create()` vs `make()` vs `makeMany()` vs `createMany()` — Porównanie

MetodaUtrwala w DBZwracaNajlepszy przypadek użycia
`create()`TakPojedyncza instancja modeluTesty funkcjonalne, seedery
`create(['key' => 'val'])`TakPojedyncza instancja modeluTesty wymagające konkretnych znanych wartości
`count(n)->create()`TakKolekcja n modeliMasowe seedowanie, testy paginacji
`make()`NiePojedyncza instancja modeluTesty jednostkowe, testowanie akcesorów/mutatorów
`make(['key' => 'val'])`NiePojedyncza instancja modeluSzybkie testy jednostkowe z kontrolowanymi atrybutami
`count(n)->make()`NieKolekcja n modeliTestowanie kolekcji w pamięci
`createMany([…])`TakKolekcjaTworzenie wsadowe z różnymi zestawami atrybutów
`makeMany([…])`NieKolekcjaWsadowe instancje w pamięci

Stany fabryki vs. nadpisania atrybutów — kiedy używać każdego z nich

ScenariuszZalecane podejście
Wielokrotnie używany warunek domenowy (np. „użytkownik admin”)Nazwana metoda stanu
Jednorazowa wartość specyficzna dla testuNadpisanie atrybutu w `create()`
Cykliczne przechodzenie przez predefiniowany zestaw`Sequence`
Efekty uboczne po utworzeniuWywołanie zwrotne `afterCreating`
Zależne wartości atrybutówAtrybuty oparte na domknięciach w `definition()`
Wypełnianie relacji`has()`, `for()`, `hasAttached()`

Praktyczna lista kontrolna decyzji

Przed napisaniem fabryki lub testu, który jej używa, przejdź przez te punkty kontrolne:

  • Czy test jest zależny od bazy danych? Jeśli nie, użyj `make()` i unikaj narzutu `RefreshDatabase`.
  • Czy test wymaga konkretnej wartości atrybutu? Przekaż ją jako nadpisanie do `create()` — nie koduj jej na stałe w fabryce `definition()`.
  • Czy testujesz zachowanie oparte na rolach? Definiuj nazwane stany zamiast rozrzucać `create(['is_admin' => true])` po wielu plikach testowych.
  • Czy wypełniasz środowisko stagingowe? Używaj `migrate:fresh –seed` i upewnij się, że Twój `DatabaseSeeder` komponuje wszystkie pod-seedery w prawidłowej kolejności zależności (rodzice przed dziećmi).
  • Czy generujesz więcej niż 5 000 rekordów? Unikaj `unique()` na polach o wysokiej kardynalności; zamiast tego używaj wartości z sufiksem UUID.
  • Czy Twoje modele mają wywołania zwrotne `afterCreating` odwołujące się do zewnętrznych usług? Mockuj te usługi w konfiguracji testu lub używaj `make()`, aby całkowicie ominąć wywołanie zwrotne.
  • Czy uruchamiasz testy równolegle? Używaj `DatabaseTransactions` zamiast `RefreshDatabase`, aby unikać konfliktów migracji między równoległymi procesami roboczymi, lub skonfiguruj oddzielne połączenia z bazą danych dla każdego procesu roboczego.

Dla zespołów zarządzających wieloma aplikacjami Laravel w różnych środowiskach, Panele Sterowania VPS zapewniają widoczność infrastruktury potrzebną do monitorowania wydajności bazy danych podczas dużych operacji seedowania i uruchomień testów.

FAQ

Jaka jest różnica między `create()` a `make()` w fabrykach Laravel?

`create()` utrwala model w bazie danych i zwraca uwodnioną instancję Eloquent. `make()` buduje model w pamięci bez żadnej interakcji z bazą danych. Używaj `make()` do czystych testów jednostkowych, aby utrzymać je szybkimi i izolowanymi; używaj `create()`, gdy test musi weryfikować stan bazy danych.

Czy fabryki Laravel obsługują relacje polimorficzne?

Tak. Zdefiniuj relację `morphTo`, ustawiając kolumny morph `*_type` i `*_id` bezpośrednio w fabryce `definition()`, lub użyj `afterCreating`, aby dołączyć relacje polimorficzne po utrwaleniu modelu nadrzędnego. Nie ma wbudowanego skrótu `hasMorphedByMany()`, więc explicite ustawianie atrybutów jest najbardziej niezawodnym podejściem.

Jak zapobiec wysyłaniu wiadomości e-mail generowanych przez fabrykę podczas testów?

Ustaw `MAIL_MAILER=array` lub `MAIL_MAILER=log` w pliku `.env.testing`. Kieruje to całą pocztę przez sterownik tablicowy lub logowy Laravel, przechwytując wiadomości w pamięci lub zapisując je do pliku dziennika bez wysyłania do serwera SMTP. Możesz następnie wykonywać asercje na `Mail::assertSent()` w swoich testach.

Dlaczego `faker->unique()->safeEmail()` rzuca `OverflowException` przy dużych zbiorach danych?

Modyfikator `unique()` Fakera utrzymuje rejestr w pamięci wcześniej wygenerowanych wartości. Gdy pula strukturalnie poprawnych unikalnych wartości zostaje wyczerpana — co może się zdarzyć przy dziesiątkach tysięcy rekordów — rzuca `OverflowException`. Rozwiązaniem jest dołączenie UUID lub losowego ciągu do bazowej wartości e-mail, zapewniając unikalność bez polegania na rejestrze Fakera.

Czy fabryki powinny być używane w seederach produkcyjnych?

Fabryki są zaprojektowane dla środowisk deweloperskich i testowych. Do seedowania produkcyjnego (np. wypełniania tabel słownikowych, domyślnych ról lub rekordów konfiguracyjnych) używaj dedykowanych klas seederów z zakodowanymi na stałe, deterministycznymi wartościami. Fabryki zależne od Fakera nigdy nie powinny być uruchamiane na produkcyjnej bazie danych, ponieważ wprowadzają nieprzewidywalne, nieaudytowalne dane.

15%

Zaoszczędź 15% na wszystkich usługach hostingowych

Sprawdź swoje umiejętności i zdobądź Rabat na dowolny plan hostingowy

Użyj kodu:

Skills
Rozpocznij