Test yang andal butuh data uji yang andal juga: user dengan email unik, post dengan relasi valid, skenario admin maupun tamu — semuanya dalam hitungan baris. Model factory adalah alat baku untuk itu, dan di dalam test ia punya trik tambahan: state untuk skenario, relasi beruntun untuk data berkeluarga, sequence untuk keberagaman, serta seeder untuk data dasar yang dipakai banyak test. Dasar factory sudah kita bahas di chapter Model Factories ; fokus kali ini adalah pemakaiannya di dalam test case.
create vs make di Dalam Test #
User::factory()->create() mengembalikan model yang sudah tersimpan ke database, sedangkan make() hanya membangun instance di memori. Test yang tidak butuh baris database sebaiknya memakai make() — lebih cepat dan tidak meninggalkan jejak:
public function test_make_membuat_instance_tanpa_menyentuh_database(): void
{
$post = Post::factory()->make([
'title' => 'Post Bayangan',
'body' => 'Hanya di memori, tidak pernah disimpan.',
]);
$this->assertSame('Post Bayangan', $post->title);
$this->assertFalse($post->exists);
$this->assertDatabaseCount('posts', 3);
}Property $post->exists bernilai false dan jumlah baris posts tidak berubah — bukti make() tidak pernah membuka transaksi INSERT. Aturan praktisnya: pakai create() ketika test akan meng-query atau me-render data itu, dan make() ketika cukup nilai atributnya.
State untuk Skenario Berbeda #
Skenario aplikasi jarang hanya punya satu wajah user. State factory mengubah beberapa atribut default tanpa mendefinisikan factory baru — cukup satu method per skenario di dalam factory:
// di database/factories/UserFactory.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,
]);
}Di dalam test, state dirangkai setelah factory() dan sebelum create() — perbandingan admin terverifikasi melawan tamu yang belum verifikasi email menjadi dua baris:
public function test_state_admin_dan_unverified(): void
{
$admin = User::factory()->admin()->create([
'email' => '[email protected]',
]);
$tamu = User::factory()->unverified()->create([
'email' => '[email protected]',
]);
$this->assertTrue($admin->is_admin);
$this->assertNotNull($admin->email_verified_at);
$this->assertFalse($tamu->is_admin);
$this->assertNull($tamu->email_verified_at);
}State di factory, bukan di test, membuat skenario itu bisa dipakai ulang di seluruh suite. Pattern sama berlaku untuk data non-user — misalnya state draft() pada PostFactory yang menganulir published_at menjadi null.
Data Berelasi dalam Satu Baris #
Fixture sering kali berbentuk keluarga data: seorang user beserta post-post-nya. Method has() merangkai factory relasi dalam satu ekspresi:
public function test_relasi_user_memiliki_tiga_post(): void
{
$siti = User::factory()
->has(Post::factory()->count(3))
->create(['email' => '[email protected]']);
$this->assertCount(3, $siti->fresh()->posts);
$this->assertDatabaseCount('posts', 6);
}Satu baris tersebut membuat user dan tiga post yang seluruh user_id-nya mengarah ke user itu — relasi hasMany teruji tanpa satu pun id yang ditulis manual. Kebalikannya, for(), memilih induk untuk child: Post::factory()->count(3)->for($user)->create(). Ingat pula definition() di PostFactory sudah berisi 'user_id' => User::factory(), sehingga setiap post yang dibuat sendirian otomatis punya penulisnya.
sequence untuk Data Beragam #
Tiga post dengan judul sama persis tidak menguji apa pun tentang pengurutan. sequence() memberi atribut berbeda untuk tiap model yang dibuat:
$posts = Post::factory()->count(3)->sequence(
['title' => 'Post Pertama'],
['title' => 'Post Kedua'],
['title' => 'Post Ketiga'],
)->create([
'user_id' => User::query()->firstWhere('email', '[email protected]')->id,
]);
$this->assertSame(
['Post Pertama', 'Post Kedua', 'Post Ketiga'],
$posts->pluck('title')->all(),
);Array pertama berlaku untuk model pertama, array kedua untuk model kedua, dan seterusnya — dikombinasikan dengan override di create() untuk atribut yang bernilai sama. Method create() sendiri mengembalikan collection sehingga urutannya bisa langsung di-assert.
Seeder: per-Test atau di setUp #
Ketika sekelompok test membutuhkan data dasar yang sama, sebaiknya seeder dijalankan sekali di setUp() daripada diulang di setiap method — DRY untuk fixture:
protected function setUp(): void
{
parent::setUp();
$this->seed(PostSeeder::class);
}
public function test_data_seeder_tersedia_di_setiap_test(): void
{
$this->assertDatabaseCount('posts', 3);
$this->assertDatabaseHas('posts', ['title' => 'Refactor dengan Tenang']);
}Setiap test dalam class ini mulai dengan satu user Budi dan tiga post dari PostSeeder — hasil transaksi RefreshDatabase yang di-rollback lalu seeder dijalankan lagi oleh setUp(). Sebaliknya, jika hanya satu dua test yang butuh data khusus, panggil $this->seed() di dalam method test itu saja agar test lain tetap ramping dan cepat.
Jalankan seluruh suite-nya:
php artisan test tests/Feature/FactoryScenarioTest.phpOutput yang Dihasilkan:
PASS Tests\Feature\FactoryScenarioTest
✓ data seeder tersedia di setiap test 0.15s
✓ state admin dan unverified 0.01s
✓ make membuat instance tanpa menyentuh database 0.01s
✓ relasi user memiliki tiga post 0.01s
✓ sequence menghasilkan data beragam 0.01s
✓ state draft menonaktifkan published at 0.01s
Tests: 6 passed (14 assertions)
Duration: 0.28sDengan factory, seeder, dan isolasi database di tangan, fondasi testing kita lengkap. Selanjutnya kita mengarahkan persenjataan ini ke wilayah yang lebih spesifik: pengujian authentication, authorization, dan API — tempat skenario login, permission, dan token menambah dimensi baru pada data uji. Rujukan lengkap ada di dokumentasi Eloquent Factories serta Database Seeders .