Test yang menulis ke database sedang mengubah keadaan bersama — dan keadaan bersama adalah musuh utama test yang andal. Bayangkan test pertama membuat user [email protected], lalu test kedua gagal karena email itu sudah terdaftar: kegagalan ini tidak ada hubungannya dengan logika test kedua. Pada chapter ini kita bedah masalah isolasi ini tuntas, memahami cara kerja trait RefreshDatabase di balik layar, dan memakai assertion database untuk memverifikasi state maupun soft delete.

Masalah Isolasi Data #

Tanpa strategi reset, dua masalah pasti muncul. Pertama, polusi antar test: data sisa dari test sebelumnya membuat test berikutnya gagal dengan alasan yang menyesatkan. Kedua, database yang tidak siap: environment test memakai SQLite :memory: yang dimulai kosong — tanpa tabel apa pun.

Yang kedua ini mudah dibuktikan. Test berikut sengaja tidak memakai trait perawatan database sama sekali:

php
class TanpaRefreshTest extends TestCase
{
    public function test_tanpa_trait_refresh_database(): void
    {
        $user = User::factory()->create();

        $this->assertTrue($user->exists);
    }
}

Output yang Dihasilkan:

text
  FAIL  Tests\Feature\TanpaRefreshTest
  ⨯  tanpa trait refresh database
  ────────────────────────────────────────────────────────────────────────────
  FAILED  Tests\Feature\TanpaRefreshTest > tanpa trait refr…  QueryException
  SQLSTATE[HY000]: General error: 1 no such table: users
  (Connection: sqlite, Database: :memory:, SQL: insert into "users" ...)

  Tests:    1 failed (0 assertions)

Factory gagal sebelum sempat menyimpan apa pun karena tabel users belum pernah dibuat di database in-memory tersebut. Test yang menyentuh Eloquent wajib menyiapkan database-nya terlebih dahulu — dan untuk itu Laravel menyediakan tiga trait.

RefreshDatabase di Balik Layar #

Trait RefreshDatabase menjalankan strategi dua lapis. Pada test pertama dalam satu proses, trait mengeksekusi migrate:fresh — seluruh tabel dibuat ulang dari nol, sekali saja. Setelah itu, setiap test dibungkus database transaction yang di-rollback otomatis setelah test selesai. Khusus SQLite :memory:, koneksi PDO-nya bahkan di-cache secara statis agar schema yang sudah dibangun tetap hidup ketika test berikutnya mengeksekusi query.

Kita bisa membuktikan transaksi tersebut benar-benar bekerja dengan dua test yang saling melanjutkan:

php
class DatabaseIsolationTest extends TestCase
{
    use RefreshDatabase;

    public function test_langkah_satu_menyimpan_satu_user(): void
    {
        User::factory()->create(['email' => '[email protected]']);

        $this->assertSame(1, User::count());
    }

    public function test_langkah_dua_mulai_dari_database_kosong(): void
    {
        $this->assertSame(0, User::count());
    }
}

Test pertama lulus karena user tersimpan; test kedua lulus karena user itu tidak ada lagi — transaksinya di-rollback. Jika transaksi tidak bekerja, test kedua akan menemukan sisa [email protected] dan gagal. Kedua test lulus berarti setiap test selalu mulai dari keadaan identik, itulah inti isolasi.

Tiga Trait, Tiga Strategi #

TraitMekanismeKapan Dipakai
RefreshDatabasemigrate:fresh sekali + transaksi per testPilihan default untuk hampir semua suite
DatabaseTransactionsTransaksi per test saja, tanpa migrasiDatabase sudah ter-migrasi (mis. database test permanen), suite besar yang ingin lebih cepat
DatabaseMigrationsmigrate:fresh sebelum tiap test + migrate:rollback setelahnyaTest yang mengubah struktur tabel secara dinamis

DatabaseTransactions tidak pernah memigrasi apa pun — cocok ketika Anda memakai database file khusus test yang sudah berisi schema. DatabaseMigrations paling lambat karena migrasi berjalan per test, tetapi menjamin keadaan benar-benar baru setiap kali.

Seeder di Dalam Test #

Terkadang test membutuhkan sekumpulan data dasar — kategori, peran, atau referensi lain yang sama untuk banyak test. Method $this->seed(PostSeeder::class) menjalankan seeder persis seperti php artisan db:seed, tetapi di dalam test dan ikut ter-rollback:

php
public function test_seeder_dijalankan_di_dalam_test(): void
{
    $this->seed(PostSeeder::class);

    $budi = User::query()->firstWhere('email', '[email protected]');

    $this->assertModelExists($budi);
    $this->assertDatabaseCount('posts', 3);
    $this->assertDatabaseHas('posts', [
        'title' => 'Belajar Laravel Testing',
    ]);
}

PostSeeder di sini membuat satu user bernama Budi beserta tiga post dengan judul eksplisit. Assertion assertModelExists memverifikasi model benar-benar ada di database melalui query exists — lebih presisi daripada assertNotNull yang hanya memeriksa variabel. Alternatifnya, deklarasikan protected bool $seed = true; agar RefreshDatabase otomatis menjalankan DatabaseSeeder setiap test.

Assertion Database dan Soft Delete #

Keluarga assertion database bekerja pada tabel, bukan model, sehingga cocok memeriksa keadaan mentah. assertDatabaseHas mencari baris dengan kolom yang cocok, assertDatabaseMissing memastikan tidak ada, assertDatabaseCount menghitung jumlah baris. Satu lagi: assertSoftDeleted memeriksa kolom deleted_at sudah terisi — berguna karena soft delete menyembunyikan baris dari query biasa tanpa menghapusnya:

php
public function test_soft_delete_menyembunyikan_post_tanpa_menghapus_baris(): void
{
    $post = Post::factory()->create(['title' => 'Post Terhapus Lembut']);
    $post->delete();

    $this->assertSoftDeleted($post);
    $this->assertDatabaseHas('posts', ['title' => 'Post Terhapus Lembut']);
    $this->assertSame(0, Post::count());
}

Tiga assertion ini menceritakan satu kejadian dari dua sudut: assertSoftDeleted membuktikan deleted_at terisi, assertDatabaseHas membuktikan barisnya masih ada di tabel (query langsung tidak menghormati scope soft delete), dan Post::count() yang bernilai 0 membuktikan query Eloquent tidak lagi melihatnya. Menjalankan seluruh suite isolasi di atas:

bash
php artisan test tests/Feature/DatabaseIsolationTest.php

Output yang Dihasilkan:

text
  PASS  Tests\Feature\DatabaseIsolationTest
  ✓  langkah satu menyimpan satu user                                    0.14s
  ✓  langkah dua mulai dari database kosong                              0.01s
  ✓  seeder dijalankan di dalam test                                     0.01s
  ✓  soft delete menyembunyikan post tanpa menghapus baris                0.01s

  Tests:    4 passed (8 assertions)
  Duration: 0.24s

Isolasi tercapai; kini pertanyaannya adalah bagaimana menyiapkan isi awal test secara efisien. Chapter berikutnya membedah factory state, relasi beruntun, dan seeder per-test — senjata utama untuk data uji. Rujukan resminya ada di dokumentasi Database Testing .