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:
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:
| Assertion | Memeriksa |
|---|---|
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:
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:
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:
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:
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:
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:
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:
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:
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:
log_mailer_waktu: 18.2 ms
log_mailer_menulis_file: true
mail_fake_waktu: 0.1 ms
mail_fake_asesi_terhitung: trueDua 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:
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:
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 .