iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0

昨天我們學會了 HTTP 測試的基礎,但你有沒有想過:「如果測試需要發送 Email 怎麼辦?」「測試時真的要上傳檔案到雲端嗎?」「測試付款功能要真的扣款嗎?」今天要學的 Mock 和 Fake 就是解決這些問題的關鍵!

本日學習地圖 🗺️

基礎階段             Kata 階段            框架特定測試
Days 1-10           Days 11-17           Days 18-27
   ✅                  ✅                  📍 Day 19

                                        HTTP 測試基礎
                                              ⬇️
                                        [Mock & Fake] <- 今天在這
                                              ⬇️
                                         資料庫測試設置

真實場景的挑戰

想像你正在測試一個訂單系統:

  1. 建立訂單後要發送確認信
  2. 付款成功要呼叫第三方 API
  3. 要上傳發票 PDF 到雲端儲存

如果每次測試都真的執行這些動作,會發生什麼事?

// ❌ 問題重重的測試
test('createsOrderAndSendsEmail', function () {
    $order = Order::create([...]);
    
    // 真的發信?測試信箱會爆滿!
    Mail::to($user)->send(new OrderConfirmation($order));
    
    // 真的扣款?測試環境也要花錢?
    $payment->charge(1000);
    
    // 真的上傳?測試檔案會塞滿儲存空間!
    Storage::put('invoices/invoice.pdf', $pdf);
});

Laravel 的 Mock 與 Fake 系統

Laravel 提供了強大的 Mock 和 Fake 機制,讓我們能安全地測試這些功能。

Fake 的基本概念

// 建立 tests/Feature/Day19/FakeBasicsTest.php
<?php

namespace Tests\Feature\Day19;

use Tests\TestCase;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Queue;

test('understandsFakeConcept', function () {
    // Fake 會攔截實際操作,記錄所有動作
    Mail::fake();
    Storage::fake('s3');
    Queue::fake();
    
    // 程式碼照常執行,但不會真的發信、上傳或排程
    // 所有動作都被記錄下來供測試驗證
});

Mail Fake:測試郵件發送

基本郵件測試

// 建立 tests/Feature/Day19/MailFakeTest.php
<?php

namespace Tests\Feature\Day19;

use Tests\TestCase;
use Illuminate\Support\Facades\Mail;
use App\Mail\Day19\WelcomeMail;

test('sendsWelcomeEmail', function () {
    Mail::fake();
    
    // 執行會發送郵件的動作
    Mail::to('user@example.com')->send(new WelcomeMail('Alice'));
    
    // 驗證郵件已發送
    Mail::assertSent(WelcomeMail::class);
    
    // 驗證發送次數
    Mail::assertSentCount(1);
    
    // 驗證收件者
    Mail::assertSent(WelcomeMail::class, function ($mail) {
        return $mail->hasTo('user@example.com');
    });
    
    // 驗證郵件內容
    Mail::assertSent(WelcomeMail::class, function ($mail) {
        return $mail->userName === 'Alice';
    });
});

test('doesNotSendEmailWhenConditionFails', function () {
    Mail::fake();
    
    $shouldSend = false;
    
    if ($shouldSend) {
        Mail::to('user@example.com')->send(new WelcomeMail('Bob'));
    }
    
    // 驗證沒有發送郵件
    Mail::assertNotSent(WelcomeMail::class);
    Mail::assertNothingSent();
});

Storage Fake:測試檔案操作

基本檔案測試

// 建立 tests/Feature/Day19/StorageFakeTest.php
<?php

namespace Tests\Feature\Day19;

use Tests\TestCase;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\UploadedFile;

test('storesUploadedFile', function () {
    Storage::fake('local');
    
    $file = UploadedFile::fake()->image('avatar.jpg', 100, 100);
    
    // 儲存檔案
    $path = Storage::disk('local')->put('avatars', $file);
    
    // 驗證檔案存在
    Storage::disk('local')->assertExists($path);
    
    // 驗證檔案不存在
    Storage::disk('local')->assertMissing('avatars/non-existent.jpg');
});

Queue Fake:測試佇列工作

基本佇列測試

// 建立 tests/Feature/Day19/QueueFakeTest.php
<?php

namespace Tests\Feature\Day19;

use Tests\TestCase;
use Illuminate\Support\Facades\Queue;
use App\Jobs\Day19\ProcessPayment;

test('dispatchesPaymentJob', function () {
    Queue::fake();
    
    // 分派工作
    ProcessPayment::dispatch(123, 99.99);
    
    // 驗證工作已分派
    Queue::assertPushed(ProcessPayment::class);
    
    // 驗證分派次數
    Queue::assertPushed(ProcessPayment::class, 1);
    
    // 驗證工作參數
    Queue::assertPushed(ProcessPayment::class, function ($job) {
        return $job->orderId === 123 
            && $job->amount === 99.99;
    });
});

HTTP Fake:測試外部 API 呼叫

基本 HTTP 測試

// 建立 tests/Feature/Day19/HttpFakeTest.php
<?php

namespace Tests\Feature\Day19;

use Tests\TestCase;
use Illuminate\Support\Facades\Http;

test('callsExternalAPI', function () {
    Http::fake([
        'api.example.com/*' => Http::response([
            'status' => 'success',
            'data' => ['id' => 1]
        ], 200)
    ]);
    
    $response = Http::get('https://api.example.com/users');
    
    expect($response->successful())->toBeTrue();
    expect($response->json('status'))->toBe('success');
    expect($response->json('data.id'))->toBe(1);
    
    // 驗證請求已發送
    Http::assertSent(function ($request) {
        return $request->url() === 'https://api.example.com/users';
    });
});

Event Fake:測試事件系統

基本事件測試

// 建立 tests/Feature/Day19/EventFakeTest.php
<?php

namespace Tests\Feature\Day19;

use Tests\TestCase;
use Illuminate\Support\Facades\Event;
use App\Events\Day19\OrderCreated;

test('dispatchesOrderCreatedEvent', function () {
    Event::fake();
    
    // 觸發事件
    OrderCreated::dispatch(456, 'customer@example.com');
    
    // 驗證事件已觸發
    Event::assertDispatched(OrderCreated::class);
    
    // 驗證事件資料
    Event::assertDispatched(OrderCreated::class, function ($event) {
        return $event->orderId === 456
            && $event->customerEmail === 'customer@example.com';
    });
});

實戰整合範例

訂單系統完整測試

// 建立 tests/Feature/Day19/OrderServiceTest.php
test('orderServiceCreatesCompleteOrder', function () {
    // 設置所有 Fake
    Mail::fake();
    Storage::fake('local');
    Event::fake();
    Queue::fake();
    
    // 執行訂單建立邏輯
    $result = createOrder(['email' => 'buyer@example.com', 'amount' => 299.99]);
    
    // 驗證所有環節
    Mail::assertSent(OrderConfirmation::class);
    Event::assertDispatched(OrderCreated::class);
    Storage::assertExists("invoices/{$result['order_id']}.txt");
    Queue::assertPushed(ProcessPayment::class);
});

小挑戰

試著實作一個通知系統,包含:

  1. 多管道通知:同時發送 Email 和 SMS
  2. 條件發送:根據用戶偏好選擇通知方式
  3. 批次處理:一次發送多個通知
test('sendsNotificationsThroughPreferredChannels', function () {
    // 實作你的測試...
});

本日重點回顧

今天我們學習了 Laravel 強大的 Mock 和 Fake 系統:

  1. Mail Fake:測試郵件發送而不真的發信
  2. Storage Fake:測試檔案操作而不真的存檔
  3. Queue Fake:測試佇列工作而不真的執行
  4. HTTP Fake:測試外部 API 而不真的呼叫
  5. Event Fake:測試事件系統而不真的觸發
    這些 Fake 機制讓我們能:
  • ✅ 安全地測試外部服務互動
  • ✅ 避免測試副作用
  • ✅ 加快測試執行速度
  • ✅ 不需要真實的外部服務

明天我們將深入資料庫測試,學習如何使用 Factory、Seeder,以及如何管理測試資料庫的狀態。準備好了嗎?讓我們繼續前進!


上一篇
Day 18 - HTTP 測試基礎 🌐
下一篇
Day 20 - 測試 TodoList 元件 📝
系列文
Laravel Pest TDD 實戰:從零開始的測試驅動開發21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言