Testing Model & Database — Testing

Testing yang Melibatkan Database Saat testing fitur yang berinteraksi dengan database, kita perlu memastikan database dalam kondisi bersih setiap test…

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);
    }
}

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:

  1. Di awal run, sekali saja, Laravel menjalankan migrate:fresh: semua tabel dihapus lalu dibuat ulang dari migration.
  2. 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'),
    ];
}

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;
    }
}

⚠️ Jebakan yang sering ketemu

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().

Yang akan kamu pelajari