Test yang menyentuh database harus mulai dari keadaan yang bersih dan bisa ditebak. Laravel memberi tiga alat untuk itu: trait RefreshDatabase (database dibersihkan untuk tiap test), factory (pabrik data contoh), dan assertion database seperti assertDatabaseHas. Bayangin dapur restoran yang dipakai koki bergantian. Tiap koki harus mulai dari meja yang sudah dilap. Kalau koki sebelumnya meninggalkan bawang di meja, masakan berikutnya ikut bau bawang, dan gak ada yang tahu kenapa.
🧪 Tebakan cepat: File test di bawah punya tiga test. Test ketiga membuat 3 user lalu mengecek isi tabelnya 3. Kalau baris
use RefreshDatabase;dihapus, test ketiga lulus atau gagal? Tebak dulu, jawabannya ada di bagian "RefreshDatabase: meja bersih untuk tiap test".
Satu file test database
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class UserTest extends TestCase
{
use RefreshDatabase;
public function test_user_bisa_dibuat(): void
{
User::factory()->create(['name' => 'Budi']);
$this->assertDatabaseHas('users', ['name' => 'Budi']);
}
public function test_user_bisa_dihapus(): void
{
$user = User::factory()->create(['name' => 'Siti']);
$user->delete();
$this->assertDatabaseMissing('users', ['name' => 'Siti']);
}
public function test_factory_membuat_tiga_user(): void
{
User::factory()->count(3)->create();
$this->assertDatabaseCount('users', 3);
}
}
use RefreshDatabase;di dalam class memasang trait-nya: tiap test mulai dari tabel kosong.User::factory()->create(['name' => 'Budi'])membuat satu user lengkap dan menyimpannya ke database. Kolom yang gak disebut (email, password) diisi factory.$this->assertDatabaseHas('users', ['name' => 'Budi']): nama tabel dulu, lalu array kolom dan nilai yang harus ada di salah satu baris.$user->delete()menghapus barisnya, laluassertDatabaseMissingmemastikan gak ada lagi baris yang cocok.User::factory()->count(3)->create()membuat 3 user sekaligus, danassertDatabaseCount('users', 3)mengecek jumlah barisnya.
Jalankan dengan php artisan test:
PASS Tests\Feature\UserTest
✓ user bisa dibuat 0.21s
✓ user bisa dihapus 0.02s
✓ factory membuat tiga user 0.02s
Tests: 3 passed (3 assertions)
Duration: 0.31s
Test pertama paling lama karena dia yang menjalankan migrasi.
RefreshDatabase: meja bersih untuk tiap test
Cara kerjanya dua langkah:
- Di awal run, sekali saja, Laravel menjalankan
migrate:fresh: semua tabel dihapus lalu dibuat ulang dari migration. - Tiap test dibungkus transaction (kumpulan perubahan yang bisa dibatalkan), lalu di-rollback (dibatalkan) begitu test-nya selesai. Apa pun yang dibuat test itu hilang lagi.
| Saat assertion jalan | Dengan RefreshDatabase |
Tanpa RefreshDatabase |
|---|---|---|
Test 1: assertDatabaseHas |
Budi (1 baris) | Budi (1 baris) |
Test 2: assertDatabaseMissing |
kosong, Siti sudah dihapus | Budi |
Test 3: assertDatabaseCount(3) |
3 user baru | Budi + 3 user baru = 4 |
Ini jawaban tebakannya: tanpa RefreshDatabase, test ketiga gagal, karena Budi dari test pertama masih ada:
⨯ factory membuat tiga user 0.03s
────────────────────────────────────────────────────────────────────────────
FAILED Tests\Feature\UserTest > factory membuat tiga user
Failed asserting that table [users] matches expected entries count of 3. Entries found: 4.
.
at tests/Feature/UserTest.php:32
28▕ public function test_factory_membuat_tiga_user(): void
29▕ {
30▕ User::factory()->count(3)->create();
31▕
➜ 32▕ $this->assertDatabaseCount('users', 3);
33▕ }
34▕ }
Jalankan sekali lagi dan angkanya jadi 8, karena data run sebelumnya juga masih tersimpan. Test yang hasilnya bergantung pada urutan dan sisa data seperti ini disebut test yang saling bocor.
Factory: pabrik data contoh
Factory menyimpan resep satu baris data, jadi test cukup menyebut kolom yang penting buatnya:
// database/factories/UserFactory.php (dibuat Laravel)
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'password' => static::$password ??= Hash::make('password'),
];
}
User::factory()->create()membuat dan menyimpan satu user.->create(['name' => 'Budi'])menimpa kolomname.User::factory()->make()cuma membuat objeknya tanpa menyimpan. Cocok untuk test yang gak butuh database.User::factory()->count(5)->create()membuat 5 user sekaligus.fake()->unique()->safeEmail()bikin email acak yang gak kembar, jadi dua user gak bentrok di kolom unik.
Assertion database
| Assertion | Lulus kalau |
|---|---|
assertDatabaseHas('users', ['name' => 'Budi']) |
ada baris dengan nilai itu |
assertDatabaseMissing('users', ['name' => 'Siti']) |
gak ada baris dengan nilai itu |
assertDatabaseCount('users', 3) |
tabelnya berisi tepat 3 baris |
assertSoftDeleted('posts', ['id' => $post->id]) |
barisnya ada, tapi deleted_at-nya terisi |
Pesan gagalnya menunjukkan isi tabel. Kalau $user->delete() lupa dipanggil di test kedua:
Failed asserting that a row in the table [users] does not match the attributes {
"name": "Siti"
}.
Found similar results: [
{
"name": "Siti"
}
].
Kalau assertDatabaseHas gak menemukan apa-apa di tabel kosong, pesannya diakhiri The table is empty.
RefreshDatabase, DatabaseTransactions, DatabaseMigrations
| Trait | Yang dilakukan | Kapan |
|---|---|---|
RefreshDatabase |
migrate:fresh sekali per run, lalu tiap test di-rollback |
pilihan bawaan |
DatabaseTransactions |
cuma membungkus tiap test dengan transaction lalu rollback, tanpa migrasi | database test-nya sudah dimigrasi sendiri |
DatabaseMigrations |
menjalankan migrasi ulang untuk tiap test | jarang, paling lambat |
Di latihan: tiru database-nya sendiri
Sandbox latihan menjalankan PHP biasa, tanpa Laravel dan tanpa database. Jadi "tabel"-nya sebuah array, dan assertion-nya kamu tulis sendiri:
class FakeDatabase
{
public array $records = [];
public function insert(array $data): bool
{
$this->records[] = $data;
return true;
}
public function has(string $key, mixed $value): bool
{
foreach ($this->records as $row) {
if ($row[$key] == $value) {
return true;
}
}
return false;
}
}
$this->records[] = $data;menambah satu baris ke array.has()menelusuri tiap baris denganforeachsampai ketemu. Kebalikannya,missing(), cukupreturn !$this->has($key, $value);, dancount()cukupreturn count($this->records);.- Pabrik data bisa ditiru dengan fungsi yang menggabungkan baris bawaan dengan nilai yang diminta:
array_merge(["name" => "User", "email" => "[email protected]"], $overrides). - Menghapus berarti lariknya benar-benar berkurang: bangun larik baru tanpa baris itu (
foreach+array_push, atauarray_filter), lalu hitung sisanya dengancount(). - Cetak hasilnya dari pemeriksaan sungguhan, misalnya
count: 2,has Budi: true, atauassertDatabaseHas(name, Budi): PASS.
⚠️ Jebakan yang sering ketemu
- Lupa
use RefreshDatabase;. Data dari test lain atau run kemarin ikut terhitung, dan test lulus atau gagal tergantung urutan. make()dikira menyimpan.User::factory()->make()gak menulis apa pun ke database, jadiassertDatabaseHassesudahnya gagal denganThe table is empty.- Argumen
assertDatabaseHasterbalik. Nama tabel dulu, baru array kolomnya:assertDatabaseHas('users', ['email' => ...]). - Yang gak ikut di-rollback. Transaction cuma membatalkan perubahan di database. Cache, antrean job dan request HTTP keluar gak ikut, jadi pakai
Cache::flush(),Queue::fake()danHttp::fake().
TL;DR: Pasang use RefreshDatabase; di class test: Laravel menjalankan migrate:fresh sekali di awal run, lalu tiap test di-rollback, jadi tiap test mulai dari tabel kosong. Buat data dengan User::factory()->create([...]) (disimpan), ->make() (gak disimpan), atau ->count(5)->create(). Periksa dengan assertDatabaseHas('users', [...]), assertDatabaseMissing('users', [...]) dan assertDatabaseCount('users', 3), nama tabel selalu di depan. Tanpa RefreshDatabase data bocor antar test: hitungan 3 jadi 4, lalu 8. DatabaseTransactions cuma rollback tanpa migrasi. Di sandbox, tabelnya array: tambah dengan $records[] = ..., cari dengan foreach, hitung dengan count().