Testing — Laravel

Laravel menyediakan tools testing yang powerful out of the box. Feature test (test HTTP endpoint): class UserTest extends TestCase { use RefreshDatabase…

Feature test di Laravel mengirim request sungguhan ke app-mu, tanpa browser dan tanpa server yang menyala, lalu memeriksa jawabannya dan isi database-nya. Bayangin uji coba sebelum perpustakaan buka. Petugas pura-pura jadi admin, mendaftarkan buku Ronggeng Dukuh Paruk di loket, lalu memeriksa dua hal: loketnya menjawab "tercatat", dan bukunya benar-benar ada di rak. Kalau loketnya bilang tercatat tapi raknya kosong, ada yang salah, dan lebih baik ketahuan sekarang daripada saat anggota datang.

🧪 Tebakan cepat: store di BookController menjawab dengan return response()->json($book);, tanpa angka status. Test-nya memeriksa assertStatus(201). Test itu lulus atau gagal, dan kalau gagal, apa yang dicetak? Tebak dulu, jawabannya ada di bagian "Menjalankan: php artisan test".

Feature test dan unit test

Feature test Unit test
Menguji satu request utuh: route, middleware, controller, database satu class atau function saja
Folder tests/Feature tests/Unit
Dibuat dengan php artisan make:test BookTest php artisan make:test BookTest --unit

Dasar PHPUnit (class ...Test, method test_..., assertion) sudah dibahas di pelajaran PHPUnit Dasar. Waktu membuat project, Laravel juga menawarkan Pest, yang menulis test tanpa class (pelajaran Testing with Pest). Di sini kita pakai PHPUnit 12, gaya yang dipakai di latihan.

Test pertama: POST /api/books

Yang diuji adalah store milik BookController, di balik route POST /api/books:

public function store(Request $request)
{
    $validated = $request->validate([
        'title' => 'required|string|max:255',
        'author' => 'required|string',
        'stock' => 'required|integer|min:0',
    ]);

    $book = Book::create($validated);

    return response()->json($book, 201);
}

Test-nya, di tests/Feature/BookTest.php:

<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class BookTest extends TestCase
{
    use RefreshDatabase;

    public function test_can_create_book(): void
    {
        $response = $this->postJson('/api/books', [
            'title' => 'Ronggeng Dukuh Paruk',
            'author' => 'Ahmad Tohari',
            'stock' => 1,
        ]);

        $response->assertStatus(201);
        $this->assertDatabaseHas('books', ['title' => 'Ronggeng Dukuh Paruk']);
    }

    public function test_validation_fails_without_title(): void
    {
        $response = $this->postJson('/api/books', [
            'author' => 'Ahmad Tohari',
            'stock' => 1,
        ]);

        $response->assertStatus(422);
    }
}

Menjalankan: php artisan test

   PASS  Tests\Feature\BookTest
  ✓ can create book                                                      0.12s
  ✓ validation fails without title                                       0.01s

  Tests:    2 passed (3 assertions)
  Duration: 0.21s

Tiap ✓ satu test yang lulus, dan namanya ditulis tanpa test_ dan garis bawah. Tiga assertion: dua di test pertama, satu di test kedua. Sekarang ubah jawaban store jadi return response()->json($book);:

   FAIL  Tests\Feature\BookTest
  ⨯ can create book                                                      0.12s
  ✓ validation fails without title                                       0.01s
  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\BookTest > can create book
  Expected response status code [201] but received 200.
Failed asserting that 200 is identical to 201.

  at tests/Feature/BookTest.php:20
  ➜  20▕         $response->assertStatus(201);

Gagal. Tanpa angka status, response()->json(...) menjawab 200, dan assertStatus(201) minta persis 201. Itu jawaban tebakannya. ⨯ menandai test yang gagal, dan ➜ menunjuk baris assertion yang berhenti. Kalau store lupa Book::create, test pertama gagal di baris 21: Failed asserting that a row in the table [books] matches the attributes {"title": "Ronggeng Dukuh Paruk"}. The table is empty.

📌 Login dan dunia luar

Route di balik middleware auth diuji dengan $this->actingAs($user)->postJson(...). Kirim email, panggil API luar, atau masukkan job ke antrean gak boleh terjadi sungguhan di test: panggil Mail::fake(), Http::fake(), atau Queue::fake() di awal test, lalu periksa dengan Mail::assertSent(...), Http::assertSent(...), atau Queue::assertPushed(...). Untuk service milikmu sendiri, serahkan versi palsunya lewat container: $this->app->instance(NotificationService::class, new FakeNotification()).

Di latihan

Latihannya menguji POST /api/products dengan bentuk yang sama: test_can_create_product mengirim "name" => "Mouse" dan "price" => 150000, lalu assertStatus(201) dan assertDatabaseHas("products", ["name" => "Mouse"]); test_validation_fails_without_name mengirim request kedua tanpa name, lalu assertStatus(422). Route POST /api/products-nya sudah ada di kode awal, dan kedua test dijalankan seperti php artisan test, jadi kalau jawabanmu benar output-nya memuat ✓ can create product, ✓ validation fails without name, dan Tests: 2 passed.

⚠️ Jebakan yang sering ketemu

TL;DR: Feature test ada di tests/Feature (php artisan make:test BookTest), unit test di tests/Unit (--unit). Class-nya extends TestCase, pakai use RefreshDatabase;, dan tiap method diawali test_. $this->postJson('/api/books', [...]) atau $this->getJson(...) mengirim request sungguhan, $response->assertStatus(201) (atau assertOk() untuk 200) memeriksa status, dan $this->assertDatabaseHas('books', [...]) memeriksa barisnya tersimpan. Satu keadaan, satu request. php artisan test mencetak ✓ untuk yang lulus, ⨯ plus pesan seperti Expected response status code [201] but received 200. dan baris yang ditunjuk ➜ untuk yang gagal.

Yang akan kamu pelajari