Pabrik Laravel: Membangun Data Uji yang Realistis dengan Model Pabrik Laravel
Saat mengembangkan aplikasi dengan Laravel, salah satu hambatan paling umum dalam alur kerja pengujian adalah menghasilkan data yang bermakna dan realistis. Laravel factories adalah kelas yang mendefinisikan cetak biru untuk membuat instance model Eloquent, menggunakan library PHP Faker untuk menghasilkan nilai atribut yang diacak namun valid secara struktural — memungkinkan pengembang untuk melakukan seed database dan menulis pengujian terisolasi tanpa harus membuat fixture data secara manual.
Berbeda dengan file seed SQL statis atau array yang dikodekan secara langsung, factories bersifat komposabel, stateful, dan menyadari relasi. Mereka terintegrasi langsung dengan suite pengujian PHPUnit dan Pest, mendukung evaluasi atribut secara lazy, dan dapat diskalakan dari satu instance model hingga ribuan record dalam satu rantai metode. Jika Anda menjalankan Laravel di lingkungan VPS Hosting, factories menjadi sangat berharga selama proses CI/CD pipeline, reset lingkungan staging, dan skenario load testing di mana pembuatan data yang dapat diulang dan terkontrol adalah suatu keharusan.
Apa Itu Laravel Factories dan Mengapa Penting
Laravel factories dirancang ulang secara fundamental di Laravel 8. Pendekatan `$factory->define()` berbasis closure yang lebih lama digantikan dengan kelas PHP khusus yang memperluas `IlluminateDatabaseEloquentFactoriesFactory`. Perubahan arsitektur ini memperkenalkan keamanan tipe, pelengkapan otomatis IDE, dan pemisahan yang lebih bersih antara logika factory dan definisi model.
Setiap kelas factory mengimplementasikan metode `definition()` yang mengembalikan array asosiatif dari atribut model. Factory secara otomatis me-resolve instance `FakerGenerator`, yang dapat diakses melalui `$this->faker`, yang mendukung lebih dari 200 penyedia data yang menyadari lokal — mulai dari `name()` dan `safeEmail()` hingga `iban()`, `latitude()`, `uuid()`, dan `creditCardNumber()`.
Kemampuan utama sistem factory modern:
- Rantai metode yang lancar untuk konfigurasi count, state, dan relasi
- Resolusi atribut lazy — closure di dalam `definition()` dievaluasi secara segar untuk setiap instance
- Factory states untuk memodelkan variasi spesifik domain (misalnya, akun yang ditangguhkan, pengguna yang terverifikasi)
- Relationship factories yang secara rekursif membuat model induk saat diperlukan
- Sequences untuk bersiklus melalui set atribut yang telah ditentukan
- `make()` vs `create()` untuk instance dalam memori vs yang dipersistensikan
Prasyarat
Sebelum mengimplementasikan factories, pastikan lingkungan Anda memenuhi persyaratan berikut:
- Laravel 9 atau lebih baru (Laravel 8 kompatibel tetapi tidak memiliki beberapa fitur sequence yang lebih baru)
- PHP 8.0 atau lebih tinggi
- Koneksi database yang dikonfigurasi di `.env` (MySQL, PostgreSQL, atau SQLite untuk pengujian dalam memori)
- Paket `laravel/framework`, yang disertakan bersama `fakerphp/faker` sebagai dependensi
- Model Eloquent dengan file migrasi yang sesuai
Untuk tim yang menjalankan Laravel di infrastruktur terkelola, VPS dengan cPanel menyediakan lingkungan yang nyaman untuk mengelola stack aplikasi dan layanan database dari antarmuka terpadu.
Langkah 1: Buat Kelas Factory
Gunakan Artisan CLI untuk membuat scaffold file factory:
“`bash
php artisan make:factory UserFactory
“`
Ini membuat `database/factories/UserFactory.php`. Jika Anda ingin secara otomatis mengaitkan factory dengan model, berikan flag `–model`:
“`bash
php artisan make:factory UserFactory –model=User
“`
Laravel me-resolve binding factory-ke-model melalui konvensi penamaan: `UserFactory` dipetakan ke `AppModelsUser`. Anda dapat mengganti ini dengan mengatur properti `protected $model` secara eksplisit, yang penting ketika model Anda berada di luar namespace `AppModels` default.
Langkah 2: Definisikan Cetak Biru Factory
Buka `database/factories/UserFactory.php` dan definisikan metode `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),
];
}
}
“`
Catatan tingkat atribut:
- `$this->faker->unique()->safeEmail()` — modifier `unique()` mempertahankan registry keunikan per-request. Jika Anda menghabiskan nilai unik yang tersedia (jarang terjadi tetapi mungkin dengan dataset yang sangat besar), Faker melempar `OverflowException`. Reset dengan `$this->faker->unique(true)` untuk menghapus cache.
- `Hash::make('password')` lebih disukai daripada `bcrypt()` secara langsung karena menghormati driver hashing yang dikonfigurasi aplikasi (bcrypt, argon2i, argon2id).
- `email_verified_at => now()` menandai pengguna sebagai sudah terverifikasi. Hilangkan field ini atau atur ke `null` untuk mensimulasikan akun yang belum terverifikasi — variasi state yang umum.
Langkah 3: Membuat Instance Model
3.1 Persistensikan Satu Record
“`php
$user = AppModelsUser::factory()->create();
“`
Ini mengeksekusi pernyataan `INSERT` dan mengembalikan model Eloquent `User` yang terhidrasi. Instance yang dikembalikan mencerminkan status database aktual, termasuk default tingkat database atau trigger apa pun.
3.2 Persistensikan Beberapa Record
“`php
$users = AppModelsUser::factory()->count(10)->create();
“`
Mengembalikan `IlluminateDatabaseEloquentCollection` dari 10 instance `User`. Setiap record menerima nilai Faker yang dihasilkan secara independen — bukan salinan dari satu instance.
3.3 Instance Dalam Memori Tanpa Persistensi
“`php
$user = AppModelsUser::factory()->make();
“`
Metode `make()` menginstansiasi model dan mengisi atributnya tanpa menyentuh database. Ini ideal untuk unit test yang memverifikasi perilaku model, casting atribut, atau logika accessor/mutator secara terisolasi — menjaga pengujian tetap cepat dan independen dari database.
3.4 Timpa Atribut Tertentu
Baik `create()` maupun `make()` menerima array penimpaan atribut:
“`php
$user = AppModelsUser::factory()->create([
'email' => 'specific@example.com',
'name' => 'Jane Doe',
]);
“`
Penimpaan mengambil prioritas atas nilai `definition()`. Ini adalah pola yang benar ketika sebuah pengujian bergantung pada nilai atribut yang spesifik dan diketahui daripada nilai acak.
Langkah 4: Factory States
States adalah modifikasi bernama pada definisi factory dasar. Mereka memungkinkan Anda memodelkan kondisi domain yang berbeda tanpa menduplikasi seluruh factory.
4.1 Mendefinisikan States
“`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 Menerapkan States
“`php
// Single state
$adminUser = AppModelsUser::factory()->admin()->create();
// Stacked states — fully composable
$suspendedAdmin = AppModelsUser::factory()->admin()->suspended()->create();
“`
States dievaluasi sesuai urutan rantainya. States yang lebih belakang menimpa kunci yang bertentangan dari states sebelumnya, memberikan resolusi atribut berlapis yang dapat diprediksi.
Langkah 5: Sequences untuk Bersiklus Melalui Nilai Atribut
Ketika Anda perlu bergantian antara set nilai yang telah ditentukan daripada nilai acak, gunakan `Sequence`:
“`php
use IlluminateDatabaseEloquentFactoriesSequence;
$users = AppModelsUser::factory()
->count(6)
->state(new Sequence(
['role' => 'editor'],
['role' => 'viewer'],
['role' => 'moderator'],
))
->create();
“`
Ini bersiklus melalui array sequence, menetapkan peran secara berurutan. Dengan 6 pengguna, setiap peran ditetapkan dua kali. Sequences sangat berharga untuk menguji pagination, kontrol akses berbasis peran, dan logika rendering UI yang bergantung pada distribusi data yang bervariasi namun terkontrol.
Langkah 6: Relationship Factories
6.1 Mendefinisikan Relasi Belongs-To
Di `PostFactory.php`, referensikan factory induk langsung sebagai nilai atribut:
“`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(),
];
}
}
“`
Ketika `user_id` diatur ke `User::factory()`, Laravel menunda evaluasinya. Jika Anda memanggil `Post::factory()->create()` tanpa menyediakan `user_id`, `User` baru secara otomatis dibuat dan primary key-nya digunakan. Jika Anda menyediakan pengguna yang sudah ada, factory bersarang dilewati sepenuhnya.
6.2 Melampirkan ke Induk yang Sudah Ada
“`php
$user = AppModelsUser::factory()->create();
$posts = AppModelsPost::factory()->count(5)->for($user)->create();
“`
Metode `for()` mengatur foreign key `user_id` ke primary key model yang disediakan, mencegah pembuatan pengguna yang tidak perlu. Ini adalah pola yang benar ketika pengujian Anda sudah memiliki pengguna tertentu dalam cakupan.
6.3 Relasi Has-Many
“`php
$userWithPosts = AppModelsUser::factory()
->has(AppModelsPost::factory()->count(3), 'posts')
->create();
“`
Atau menggunakan singkatan magic `hasPosts()` (di-resolve melalui nama metode relasi pada model):
“`php
$userWithPosts = AppModelsUser::factory()->hasPosts(3)->create();
“`
Ini membuat satu pengguna dan tiga post yang terkait dalam satu operasi atomik — dengan semua foreign key di-resolve dengan benar.
6.4 Relasi Many-to-Many
“`php
$user = AppModelsUser::factory()
->hasAttached(
AppModelsRole::factory()->count(2),
['assigned_at' => now()]
)
->create();
“`
Metode `hasAttached()` menangani penyisipan tabel pivot, termasuk atribut pivot tambahan apa pun yang perlu Anda isi.
Langkah 7: Menggunakan Factories dalam Pengujian
7.1 Feature Test dengan Pernyataan Database
“`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');
}
}
“`
Detail penting: Selalu gunakan trait `RefreshDatabase` atau `DatabaseTransactions` di kelas pengujian yang berinteraksi dengan database. `RefreshDatabase` menjalankan migrasi secara segar sebelum suite pengujian dan membungkus setiap pengujian dalam transaksi yang di-rollback setelahnya, menjaga pengujian tetap terisolasi dan idempoten.
7.2 Unit Test dengan `make()`
“`php
public function test_user_full_name_accessor(): void
{
$user = AppModelsUser::factory()->make([
'name' => 'Alice Wonderland',
]);
$this->assertEquals('Alice Wonderland', $user->name);
}
“`
Tidak ada interaksi database yang terjadi. Pengujian berjalan dalam milidetik dan cocok untuk pipeline CI dengan frekuensi tinggi.
Langkah 8: Database Seeders dengan Factories
8.1 Buat Seeder
“`bash
php artisan make:seeder UserSeeder
“`
8.2 Implementasikan Seeder
“`php
<?php
namespace DatabaseSeeders;
use AppModelsUser;
use IlluminateDatabaseSeeder;
class UserSeeder extends Seeder
{
public function run(): void
{
User::factory()
->count(50)
->create();
}
}
“`
8.3 Composite Seeder dengan Relasi
“`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 Jalankan Seeder
“`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
“`
Perintah `migrate:fresh –seed` adalah alur kerja standar untuk mereset database staging atau pengembangan ke kondisi yang diketahui dan terisi. Di Dedicated Servers, pola ini sering digunakan sebelum siklus QA untuk memastikan lingkungan yang bersih dan dapat direproduksi.
Pola Lanjutan dan Kasus Tepi
Atribut Lazy dan Nilai Dependen
Nilai Faker di dalam `definition()` dievaluasi ulang untuk setiap panggilan factory. Namun, jika Anda memerlukan satu atribut untuk bergantung pada atribut lain, gunakan closure:
“`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('###'),
];
}
“`
Ini memastikan `email` dan `username` diturunkan dari nilai nama yang sama, menghasilkan record yang konsisten secara internal.
Menghindari Jebakan Overflow `unique()`
Saat menghasilkan dataset besar (10.000+ record dalam satu panggilan factory), `$this->faker->unique()->safeEmail()` dapat menghabiskan pool keunikan Faker dan melempar `OverflowException`. Mitigasi ini dengan menambahkan UUID atau timestamp ke nilai yang dihasilkan:
“`php
'email' => $this->faker->safeEmail() . '.' . $this->faker->uuid() . '@test.com',
“`
Ini menjamin keunikan pada skala besar tanpa bergantung pada registry keunikan internal Faker.
Factory Callbacks: `afterMaking` dan `afterCreating`
Gunakan callbacks untuk melakukan logika pasca-pembuatan yang tidak dapat diekspresikan sebagai atribut sederhana:
“`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'),
]);
});
}
“`
Metode `configure()` dipanggil sekali ketika factory diinstansiasi. `afterCreating` berjalan setelah model dipersistensikan, menjadikannya cocok untuk membuat model terkait yang memerlukan primary key induk.
Menguji Fungsionalitas Email
Ketika factories menghasilkan pengguna dengan alamat email, pengujian integrasi yang memverifikasi pengiriman email mendapat manfaat dari lingkungan Email Hosting khusus yang dikonfigurasi dengan server SMTP sandbox, mencegah pengiriman email pengujian secara tidak sengaja ke alamat nyata.
`create()` vs `make()` vs `makeMany()` vs `createMany()` — Perbandingan
| Metode | Persistensi ke DB | Mengembalikan | Kasus Penggunaan Terbaik |
|---|---|---|---|
| — | — | — | — |
| `create()` | Ya | Instance model tunggal | Feature test, seeders |
| `create(['key' => 'val'])` | Ya | Instance model tunggal | Pengujian yang memerlukan nilai tertentu yang diketahui |
| `count(n)->create()` | Ya | Koleksi n model | Seeding massal, pengujian pagination |
| `make()` | Tidak | Instance model tunggal | Unit test, pengujian accessor/mutator |
| `make(['key' => 'val'])` | Tidak | Instance model tunggal | Unit test cepat dengan atribut terkontrol |
| `count(n)->make()` | Tidak | Koleksi n model | Pengujian koleksi dalam memori |
| `createMany([…])` | Ya | Koleksi | Pembuatan batch dengan set atribut yang berbeda |
| `makeMany([…])` | Tidak | Koleksi | Instance dalam memori secara batch |
Factory States vs. Penimpaan Atribut — Kapan Menggunakan Masing-masing
| Skenario | Pendekatan yang Direkomendasikan |
|---|---|
| — | — |
| Kondisi domain yang dapat digunakan kembali (misalnya, “pengguna admin”) | Metode state bernama |
| Nilai satu kali yang spesifik untuk pengujian | Penimpaan atribut di `create()` |
| Bersiklus melalui set yang telah ditentukan | `Sequence` |
| Efek samping pasca-pembuatan | Callback `afterCreating` |
| Nilai atribut dependen | Atribut berbasis closure di `definition()` |
| Pengisian relasi | `has()`, `for()`, `hasAttached()` |
Daftar Periksa Keputusan Praktis
Sebelum menulis factory atau pengujian yang menggunakannya, kerjakan poin-poin berikut:
- Apakah pengujian bergantung pada database? Jika tidak, gunakan `make()` dan hindari overhead `RefreshDatabase`.
- Apakah pengujian memerlukan nilai atribut tertentu? Berikan sebagai penimpaan ke `create()` — jangan dikodekan langsung di factory `definition()`.
- Apakah Anda menguji perilaku berbasis peran? Definisikan states bernama daripada menyebarkan `create(['is_admin' => true])` di beberapa file pengujian.
- Apakah Anda melakukan seeding lingkungan staging? Gunakan `migrate:fresh –seed` dan pastikan `DatabaseSeeder` Anda menyusun semua sub-seeder dalam urutan dependensi yang benar (induk sebelum anak).
- Apakah Anda menghasilkan lebih dari 5.000 record? Hindari `unique()` pada field dengan kardinalitas tinggi; gunakan nilai dengan sufiks UUID sebagai gantinya.
- Apakah model Anda memiliki callback `afterCreating` yang mengakses layanan eksternal? Mock layanan tersebut dalam setup pengujian Anda atau gunakan `make()` untuk melewati callback sepenuhnya.
- Apakah Anda menjalankan pengujian secara paralel? Gunakan `DatabaseTransactions` daripada `RefreshDatabase` untuk menghindari konflik migrasi di seluruh worker paralel, atau konfigurasikan koneksi database terpisah per worker.
Untuk tim yang mengelola beberapa aplikasi Laravel di berbagai lingkungan, VPS Control Panels menyediakan visibilitas infrastruktur yang diperlukan untuk memantau performa database selama operasi seeding besar dan proses pengujian.
FAQ
Apa perbedaan antara `create()` dan `make()` di Laravel factories?
`create()` mempersistensikan model ke database dan mengembalikan instance Eloquent yang terhidrasi. `make()` membangun model dalam memori tanpa interaksi database apa pun. Gunakan `make()` untuk unit test murni agar tetap cepat dan terisolasi; gunakan `create()` ketika pengujian harus memverifikasi status database.
Bisakah Laravel factories menangani relasi polimorfik?
Ya. Definisikan relasi `morphTo` dengan mengatur kolom morph `*_type` dan `*_id` langsung di factory `definition()`, atau gunakan `afterCreating` untuk melampirkan relasi polimorfik setelah model induk dipersistensikan. Tidak ada singkatan `hasMorphedByMany()` bawaan, sehingga pengaturan atribut eksplisit adalah pendekatan yang paling andal.
Bagaimana cara mencegah email yang dihasilkan factory dikirim selama pengujian?
Atur `MAIL_MAILER=array` atau `MAIL_MAILER=log` di file `.env.testing` Anda. Ini merutekan semua email melalui driver array atau log Laravel, menangkap pesan dalam memori atau menulisnya ke file log tanpa mengirimkan ke server SMTP. Anda kemudian dapat melakukan assertion pada `Mail::assertSent()` dalam pengujian Anda.
Mengapa `faker->unique()->safeEmail()` melempar `OverflowException` pada dataset besar?
Modifier `unique()` Faker mempertahankan registry dalam memori dari nilai yang sebelumnya dihasilkan. Ketika pool nilai unik yang valid secara struktural habis — yang dapat terjadi dengan puluhan ribu record — ia melempar `OverflowException`. Solusinya adalah menambahkan UUID atau string acak ke nilai email dasar, memastikan keunikan tanpa bergantung pada registry Faker.
Haruskah factories digunakan dalam seeder produksi?
Factories dirancang untuk lingkungan pengembangan dan pengujian. Untuk seeding produksi (misalnya, mengisi tabel lookup, peran default, atau record konfigurasi), gunakan kelas seeder khusus dengan nilai deterministik yang dikodekan secara langsung. Factories yang bergantung pada Faker tidak boleh pernah dijalankan terhadap database produksi, karena mereka memperkenalkan data yang tidak dapat diprediksi dan tidak dapat diaudit.
