Selama section ini kita memakai mailer log sebagai alat bukti — membaca storage/logs/laravel.log setelah setiap pengiriman. Pendekatan itu pas untuk eksplorasi manual, tetapi bukan untuk test suite: log adalah teks tak berstruktur yang harus diparsing, berkasnya menumpuk antar-test, dan tidak ada assertion siap pakai untuk “apakah email ke Siti pernah terkirim”. Laravel menyediakan lapisan khusus untuk pengujian: Mail::fake() dan Notification::fake() mengganti transport dengan objek pencatat yang menyimpan setiap pesan di memori dan menyediakan assertion bermata-langit langsung. Pada chapter penutup ini kita rakit suite test lengkap yang mencakup mailable, notifikasi multi-channel, routing, sampai antrean — tanpa satu byte pun menyentuh jaringan.

Mail::fake dan assertSent #

Test dasar memalsukan mail lalu mengirim mailable persis seperti production:

php
use App\Mail\OrderShipped;
use Illuminate\Support\Facades\Mail;

Mail::fake();

Mail::to('[email protected]')->send(new OrderShipped('123', 'Budi'));

Mail::assertSent(OrderShipped::class, 1);

Baris terakhir memeriksa bahwa tepat satu OrderShipped terkirim. Assertion keluarga Mail::fake yang tersedia:

AssertionMemeriksa
assertSent($mailable, $jumlah)Terkirim sebanyak N
assertNotSent($mailable, $closure)Tidak terkirim (atau tidak cocok closure)
assertNothingSent()Tidak ada kiriman sama sekali
assertQueued($mailable, $jumlah)Masuk antrean sebanyak N
assertNotQueued($mailable)Tidak masuk antrean
assertNothingQueued()Antrean kosong

Bentuk paling kuat adalah closure inspeksi — argument kedua menerima instance mailable yang tertangkap sehingga property, envelope, dan penerima bisa diperiksa satu per satu:

php
Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) {
    return $mail->envelope()->subject === 'Pesanan #123 Telah Dikirim'
        && $mail->orderId === '123'
        && $mail->namaPembeli === 'Budi'
        && $mail->hasTo('[email protected]');
});

Method hasTo(), hasCc(), hasBcc(), dan hasReplyTo() tersedia di setiap mailable untuk memeriksa amplop tanpa membaca header mentah. Test lengkapnya berjalan di sandbox:

php
public function test_mailable_terkirim_dengan_subject_dan_data_yang_benar(): void
{
    Mail::fake();

    Mail::to('[email protected]')->send(new OrderShipped('123', 'Budi'));

    Mail::assertSent(OrderShipped::class, 1);
    Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) {
        return $mail->envelope()->subject === 'Pesanan #123 Telah Dikirim'
            && $mail->orderId === '123'
            && $mail->namaPembeli === 'Budi'
            && $mail->hasTo('[email protected]');
    });

    fwrite(STDERR, "mailable_test: lulus (subject + data + penerima terverifikasi)\n");
}

Output yang Dihasilkan:

text
mailable_test: lulus (subject + data + penerima terverifikasi)

  PASS  Tests\Feature\MailableTest
  ✓  mailable terkirim dengan subject dan data yang benar                0.08s

  Tests:    1 passed (2 assertions)

Mailable Berantrean #

Mail::to()->queue() mengirim mailable lewat antrean. Fake membedakan “terkirim” dan “masuk antrean” sebagai dua keadaan berbeda — dan assertNothingSent memastikan tidak ada yang bocor terkirim langsung:

php
Mail::fake();

Mail::to('[email protected]')->queue(new OrderShipped('123', 'Budi'));

Mail::assertQueued(OrderShipped::class, 1);
Mail::assertQueued(OrderShipped::class, function (OrderShipped $mail) {
    fwrite(STDERR, 'queued_subject: ' . $mail->envelope()->subject . "\n");

    return $mail->orderId === '123';
});
Mail::assertNothingSent();

fwrite(STDERR, "queued_mailable_test: lulus\n");

Output yang Dihasilkan:

text
queued_subject: Pesanan #123 Telah Dikirim
queued_mailable_test: lulus

  PASS  Tests\Feature\OrderShippedQueuedTest
  ✓  mailable masuk antrean bukan langsung terkirim                    0.14s

  Tests:    1 passed (3 assertions)

Closure assertQueued menerima instance mailable yang sama seperti assertSent — subject dan data tetap bisa diperiksa meski email belum “berangkat”.

Notification::fake dan assertSentTo #

Sistem notifikasi punya fake sendiri karena targetnya berbeda: bukan alamat email, melainkan entitas notifiable. assertSentTo menerima user (atau koleksi user) plus class notifikasi:

php
use App\Models\User;
use App\Notifications\OrderPaid;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;

public function test_notifikasi_hanya_ke_user_tertentu(): void
{
    Notification::fake();

    $budi = User::factory()->create(['name' => 'Budi', 'email' => '[email protected]']);
    $siti = User::factory()->create(['name' => 'Siti', 'email' => '[email protected]']);

    Notification::send([$budi], new OrderPaid('123', 775000));

    Notification::assertSentTo($budi, OrderPaid::class);
    Notification::assertNotSentTo($siti, OrderPaid::class);

    Notification::assertSentTo($budi, OrderPaid::class, function ($n, $channels) {
        fwrite(STDERR, 'channels: ' . implode(', ', $channels) . "\n");

        return in_array('database', $channels);
    });

    fwrite(STDERR, "channel_test: lulus\n");
}

Output yang Dihasilkan:

text
channels: database, mail
channel_test: lulus

  PASS  Tests\Feature\NotificationChannelTest
  ✓  notifikasi hanya ke user tertentu                                   0.15s

  Tests:    1 passed (3 assertions)

Closure inspeksi menerima dua argument: instance notifikasi dan array channel yang aktif untuk penerima tersebut — inilah cara menguji routing via() tanpa menjalankan channel sungguhan. Asersi assertNotSentTo($siti, ...) memastikan kebocoran penerima tertangkap. Assertion lain yang tersedia: assertSentToTimes, assertSentOnDemand untuk notifikasi Notification::route(), assertNothingSent, dan assertCount.

Mengapa Fake Mengalahkan Log Mailer di Test #

Log mailer terasa cukup — email tetap “terkirim” dan bisa dibaca. Runner berikut membandingkan keduanya secara langsung:

php
use App\Mail\OrderShipped;
use Illuminate\Support\Facades\Mail;

// --- jalur log mailer (menulis ke storage/logs) ---
@unlink($dir.'/storage/logs/laravel.log');
$t0 = microtime(true);
Mail::to('[email protected]')->send(new OrderShipped('123', 'Budi'));
$t1 = microtime(true);
echo 'log_mailer_waktu: ' . round(($t1 - $t0) * 1000, 1) . " ms\n";
echo 'log_mailer_menulis_file: ' . var_export(
    file_exists($dir.'/storage/logs/laravel.log'), true) . "\n";

// --- jalur Mail::fake ---
Mail::fake();
$t2 = microtime(true);
Mail::to('[email protected]')->send(new OrderShipped('456', 'Siti'));
$t3 = microtime(true);
echo 'mail_fake_waktu: ' . round(($t3 - $t2) * 1000, 1) . " ms\n";
echo 'mail_fake_asesi_terhitung: ' . var_export(
    Mail::hasSent(OrderShipped::class), true) . "\n";

Output yang Dihasilkan:

text
log_mailer_waktu: 18.2 ms
log_mailer_menulis_file: true
mail_fake_waktu: 0.1 ms
mail_fake_asesi_terhitung: true

Dua ratus kali lebih cepat hanyalah bonus. Pembeda utamanya: log mailer menulis berkas yang harus dibersihkan antar-test dan memerlukan parsing regex untuk memeriksa isi, sementara fake menyimpan pesan sebagai objek PHP yang bisa diperiksa lewat assertion bertipe — hasTo('[email protected]') adalah method sungguhan, bukan str_contains() di atas teks log.

Suite Gabungan Lengkap #

Test penutup menggabungkan kedua fake dalam satu skenario utuh — email transaksional plus notifikasi multi-channel, dengan pemeriksaan silang penerima:

php
public function test_suite_lengkap_mail_dan_notification_fake(): void
{
    Mail::fake();
    Notification::fake();

    $budi = User::factory()->create(['name' => 'Budi', 'email' => '[email protected]']);
    $siti = User::factory()->create(['name' => 'Siti', 'email' => '[email protected]']);

    Mail::to($budi)->send(new OrderShipped('123', 'Budi'));
    $budi->notify(new OrderPaid('123', 775000));

    // 1. Mailable terkirim + konten terverifikasi
    Mail::assertSent(OrderShipped::class, 1);
    Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) use ($budi) {
        return $mail->hasTo($budi->email)
            && $mail->envelope()->subject === 'Pesanan #123 Telah Dikirim';
    });

    // 2. Tidak ada email yang bocor ke penerima lain
    Mail::assertNotSent(OrderShipped::class, function (OrderShipped $mail) {
        return $mail->hasTo('[email protected]');
    });

    // 3. Notifikasi sampai ke Budi, tidak ke Siti
    Notification::assertSentTo($budi, OrderPaid::class);
    Notification::assertNotSentTo($siti, OrderPaid::class);

    fwrite(STDERR, "email_test_suite: 3 kelompok asersi lulus semua\n");
}

Output yang Dihasilkan:

text
email_test_suite: 3 kelompok asersi lulus semua

  PASS  Tests\Feature\PengujianEmailTest
  ✓  suite lengkap mail dan notification fake                           0.15s

  Tests:    1 passed (5 assertions)

Satu test memverifikasi lintas sistem: mailable hanya ke Budi dengan subject benar, email tidak bocor ke Siti, dan notifikasi multi-channel sampai ke penerima yang tepat. Setiap assertion berjalan tanpa file log, tanpa SMTP, tanpa worker — hanya PHP in-memory.

Dengan ini perjalanan email dan notifikasi kita lengkap: konfigurasi, pengiriman, mailable, template, lampiran, notifikasi multi-channel, antrean, dan pengujian. Langkah alami berikutnya di dunia nyata adalah penyimpanan dan pemrosesan file berbasis cloud — tema yang dibahas pada section Filesystem. Daftar assertion lengkap untuk mail dan notifikasi ada di dokumentasi Mocking serta dokumentasi Mail .