Test yang baik cepat dan deterministik — dan musuh utamanya adalah dunia luar: API yang lambat, layanan cuaca yang berubah-ubah, jam yang terus berjalan. Strateginya adalah mengganti dependency di boundary (jaringan, waktu, filesystem, antrean) dengan versi terkendali. Laravel menyediakan dua keluarga alat: facade fake bawaan untuk infrastruktur Laravel sendiri, dan Mockery — sudah terpasang — untuk service kelas kita. Pada chapter ini kita pakai keduanya pada satu service nyata yang memanggil API eksternal, plus kontrol waktu dengan Carbon::setTestNow() dan travel().

Kapan Kita Mock #

Aturan praktisnya sederhana: mock dependency yang kita tidak punya dan yang tidak menentukan kebenaran kode kita. Service cuaca milik pihak ketiga tidak boleh ikut diuji — kalau API itu mati, suite kita gagal untuk alasan yang salah. Sebaliknya, query database atau perhitungan internal justru harus tetap asli, karena itulah kode yang sedang kita verifikasi.

Laravel sudah membekali semua boundary utama dengan fake resmi:

BoundaryFakeAssertion kunci
AntreanQueue::fake()assertPushed, assertPushedOn
Command bus / chainBus::fake()assertChained, assertDispatchedSync
EventEvent::fake()assertDispatched
NotifikasiNotification::fake()assertSentTo
EmailMail::fake()assertSent, assertQueued
FilesystemStorage::fake()assertExists, assertMissing
HTTP keluarHttp::fake()assertSent, assertNothingSent
WaktuCarbon::setTestNow(), travel()tak perlu — cukup assert hasil

Chapter-chapter sebelumnya telah memakai lima yang pertama; dua terakhir dan Mockery menjadi fokus kita di sini.

Http::fake: Jaringan yang Selalu Menurut #

Service cuaca kita memanggil https://api.example.com/cuaca/{kota} dan mengembalikan suhu. Http::fake() mencegat request tersebut tanpa menyentuh jaringan:

php
public function test_http_fake_menggantikan_jaringan(): void
{
    Http::fake(['api.example.com/*' => ['suhu' => 31]]);

    $this->get('/cuaca/jakarta')
        ->assertOk()
        ->assertSee('Cuaca jakarta: 31 derajat. Saran: kaos.');

    Http::assertSent(function ($request) {
        return $request->url() === 'https://api.example.com/cuaca/jakarta';
    });
}

Controller memanggil service sungguhan, service memanggil HTTP client sungguhan — hanya lapisan jaringan yang diganti. Http::assertSent menambah bukti bahwa URL yang dituju benar, jadi typo domain pun tertangkap.

Mock Service di Container dengan Mockery #

Untuk mengganti service pada level yang lebih tinggi, $this->mock() mengikat instance Mockery ke container, dan dependency injection di controller otomatis menerimanya:

php
public function test_mock_menggantikan_service_di_container(): void
{
    $this->mock(CuacaService::class, function ($mock) {
        $mock->shouldReceive('suhuTerbaru')->once()->with('jakarta')->andReturn(27);
        $mock->shouldReceive('saranPakaian')->once()->with(27)->andReturn('payung');
    });

    $this->get('/cuaca/jakarta')
        ->assertOk()
        ->assertSee('Cuaca jakarta: 27 derajat. Saran: payung.');
}

Rantai shouldReceive('suhuTerbaru')->once()->with('jakarta')->andReturn(27) adalah kontrak lengkap: method apa dipanggil, berapa kali, argumen apa, dan hasil apa. Mock penuh men-stub semua method — karena itu dua method yang dipakai controller kita definisikan keduanya, sebab method yang tidak di-stub mengembalikan null.

Partial Mock dan Spy #

Ketika service punya method murni yang logikanya justru ingin ikut diuji, partialMock() hanya men-stub yang disebut:

php
public function test_partial_mock_menjaga_method_asli(): void
{
    $this->partialMock(CuacaService::class)
        ->shouldReceive('suhuTerbaru')
        ->andReturn(33);

    $this->get('/cuaca/jakarta')
        ->assertOk()
        ->assertSee('Cuaca jakarta: 33 derajat. Saran: kaos.');
}

Method suhuTerbaru dipalsukan (33), tetapi saranPakaian(33) tetap menjalankan kode asli — output kaos (bukan null) membuktikan method asli ikut bekerja. Spy bekerja sebaliknya dari mock: tidak menyetel ekspektasi di depan, hanya merekam:

php
public function test_spy_merekam_panggilan_tanpa_eksekusi_asli(): void
{
    $spy = $this->spy(CuacaService::class);

    $respons = $this->get('/cuaca/jakarta');

    $respons->assertOk()->assertSee('Cuaca jakarta: 0 derajat. Saran: .');

    $spy->shouldHaveReceived('suhuTerbaru')
        ->once()
        ->with('jakarta');
}

Perhatikan isi responsnya: 0 derajat dan Saran: . — semua method spy mengembalikan null, sehingga (int) null menjadi 0. Verifikasi shouldHaveReceived dilakukan setelah aksi berjalan. Ringkasnya: mock itu menagih di depan (“kamu harus memanggil ini, begini hasilnya”), spy menyidik di belakang (“apa yang sebenarnya terjadi?”).

Mengendalikan Waktu #

Boundary terakhir adalah jam. Route promo kita menghitung sisa hari menuju tanggal tetap; tanpa kontrol waktu, hasilnya berubah setiap hari. Carbon::setTestNow() membekukan jam, travel() memindahkannya:

php
public function test_set_test_now_membekukan_waktu(): void
{
    Carbon::setTestNow('2026-01-15 00:00:00');

    $this->get('/promo/hari')->assertOk()->assertSee('dalam 5 hari');

    $this->travel(6)->days();

    $this->get('/promo/hari')->assertOk()->assertSee('dalam -1 hari');

    $this->travelBack();
    Carbon::setTestNow();
}

public function test_travel_to_melompat_ke_momen_tertentu(): void
{
    $this->travelTo(Carbon::parse('2026-01-18 00:00:00'));

    $this->get('/promo/hari')->assertOk()->assertSee('dalam 2 hari');

    $this->travelBack();
}

Dengan jam dibekukan di 15 Januari 2026, sisa promo pasti 5 hari; maju 6 hari membuatnya -1 (sudah lewat). Pembersihan di akhir — travelBack() dan Carbon::setTestNow() tanpa argumen — penting agar pembekuan tidak bocor ke test lain dalam satu proses.

Seluruh Suite Berjalan #

bash
php artisan test tests/Feature/MockingTest.php

Output yang Dihasilkan:

text
  PASS  Tests\Feature\MockingTest
  ✓  http fake menggantikan jaringan                                      0.16s
  ✓  mock menggantikan service di container                                 0.01s
  ✓  partial mock menjaga method asli                                       0.01s
  ✓  spy merekam panggilan tanpa eksekusi asli                             0.01s
  ✓  set test now membekukan waktu                                         0.01s
  ✓  travel to melompat ke momen tertentu                                  0.01s

  Tests:    6 passed (18 assertions)
  Duration:  0.28s

Suite kita kini tuntas dari sisi teknik: feature, HTTP, database, auth, API, upload, queue, event, mock. Pertanyaan terakhir bersifat manajerial — seberapa banyak kode yang benar-benar tersentuh test? Chapter Test Coverage menutup section ini dengan cara mengukurnya dan membaca angkanya secara kritis. Rujukan lengkap ada di dokumentasi Mocking .