Mockery 在 Laravel 中掌握假物件的藝術

更新於 發佈於 閱讀時間約 18 分鐘

在〈Laravel Test 實戰:與框架結合的測試技巧 〉那篇文章裡,我們證明了「光靠 Laravel 內建工具」,就能用 assertDatabaseHas()Http::fake()Mail::fake() 等方式覆蓋多數業務流程,幫團隊跨出測試第一步。然而,當專案走向多層 Service、鏈式 SDK 與第三方套件時,你很快就會遇到另一層瓶頸——要連真實資料庫、呼叫外部 API,甚至真的寄出 Email,測試跑得慢又容易失敗。

真正的難題不是技術,而是 缺少一套成熟且可精準驗證的替身(Mock)機制。因此,本篇以 Laravel 為基準,帶你用更進階的 Mockery 補足內建 fake 的不足;除了講清楚原理,也準備了 4 組可直接貼上的模擬範例,讓你在不碰真實依賴的情況下,依舊能驗證呼叫次數、參數內容,甚至攔截鏈式方法,把測試覆蓋率一路推進到核心商業邏輯。


為什麼 Laravel 仍需要 Mockery?

Laravel 內建 Http::fake()Mail::fake(),但在以下情境仍力有未逮:

  • 鏈式 SDK: $gateway->auth()->charge()
  • 複雜邏輯依賴: Repository、Service Layer
  • 跨專案共用套件: 純 PHP 套件無法用 Laravel fake
  • 呼叫次數、引數驗證: 需要精準斷言「呼叫幾次」「帶什麼參數」

Mockery 已被列入 Laravel 的 require-dev,並且 Tests\TestCase 會在 tearDown() 自動呼叫 Mockery::close(),與框架無縫結合。


環境確認(Laravel 8 以上,預設已內建)

composer show mockery/mockery

若能看到版本號(例如 1.6.x),表示框架已內建,不需額外安裝; 只有在 舊版專案純 PHP 套件 才需要手動 composer require mockery/mockery 並自行設定 listener。


範例 1:Mock Mailer 發送電子郵件

<?php

namespace Tests\Feature;

use App\Services\InvoiceService;
use App\Services\MailerInterface;
use Mockery as m;
use Tests\TestCase;

class InvoiceTest extends TestCase
{
public function test_receipt_mail_is_sent_on_payment()
{
// 準備 Mock 物件
$mailer = m::mock(MailerInterface::class);

// 定義期望行為
$mailer->shouldReceive('sendReceipt')
->once()
->withArgs(function ($user, $amount) {
// 驗證參數是否符合預期
return $user->id === 1 && $amount === 999;
})
->andReturnTrue();

// 將 Mock 注入到測試目標中
$this->app->instance(MailerInterface::class, $mailer);

// 執行測試
$service = $this->app->make(InvoiceService::class);
$user = \App\Models\User::find(1) ?? \App\Models\User::factory()->create();

// 驗證結果
$this->assertTrue($service->processPayment($user, 999));
}
}

本例展示了:

  • 精確控制方法呼叫次數(once()
  • 驗證傳入參數(withArgs()
  • 自定義回傳值(andReturnTrue()

範例 2:驗證外部支付閘道 API 呼叫

<?php

namespace Tests\Unit;

use App\Services\PaymentService;
use App\Gateways\PaymentGatewayInterface;
use Mockery as m;
use Tests\TestCase;

class PaymentServiceTest extends TestCase
{
public function test_payment_gateway_receives_correct_arguments()
{
// 建立 Mock 物件
$gateway = m::mock(PaymentGatewayInterface::class);

// 設定精確的參數驗證
$gateway->shouldReceive('charge')
->withArgs(function (int $amount, string $currency, array $options) {
// 驗證金額、幣別和選項
return $amount === 1000 &&
$currency === 'TWD' &&
isset($options['description']) &&
$options['capture'] === true;
})
->once()
->andReturn([
'transaction_id' => 'tx_12345',
'status' => 'success',
'created_at' => now()->toIso8601String()
]);

$this->app->instance(PaymentGatewayInterface::class, $gateway);

// 執行測試目標方法
$service = $this->app->make(PaymentService::class);
$result = $service->processCharge(1000, 'TWD', '商品購買');

// 驗證結果
$this->assertEquals('success', $result['status']);
$this->assertEquals('tx_12345', $result['transaction_id']);
}
}

本例展示了:

  • 複雜參數驗證(數字、字串、陣列)
  • 模擬真實的 API 回應結構
  • 驗證服務層如何正確處理外部依賴

範例 3:模擬鏈式 SDK 調用

<?php

namespace Tests、Unit;

use App\Services\NotificationService;
use App\Clients\FirebaseClient;
use Mockery as m;
use Tests\TestCase;

class NotificationServiceTest extends TestCase
{
public function test_firebase_push_notification_chain()
{
// 建立鏈式 Mock
$firebaseClient = m::mock(FirebaseClient::class);

// 設定鏈式呼叫的期望
$firebaseClient->shouldReceive('connect->project->messaging->send')
->once()
->withArgs(function (array $notification) {
return $notification['topic'] === 'new_updates' &&
!empty($notification['title']) &&
!empty($notification['body']);
})
->andReturn([
'message_id' => 'projects/myapp/messages/msg_123',
'success' => true
]);

$this->app->instance(FirebaseClient::class, $firebaseClient);

// 執行被測試方法
$service = $this->app->make(NotificationService::class);
$result = $service->sendPushNotification(
'new_updates',
'系統更新通知',
'您的應用已更新至最新版本'
);

// 驗證結果
$this->assertTrue($result);
}
}

本例展示:

  • 鏈式方法調用的模擬(connect->project->messaging->send
  • 複雜結構的參數驗證
  • 模擬成功響應場景

範例 4:部分替身(Partial Mock)和 Spy 監聽

<?php

namespace Tests\Feature;

use App\Services\HeartbeatService;
use App\Repositories\UserRepository;
use Mockery as m;
use Mockery\MockInterface;
use Tests\TestCase;

class HeartbeatTest extends TestCase
{
public function test_heartbeat_updates_last_seen_and_cache()
{
// 取得認證使用者
$user = \App\Models\User::factory()->create();

// 建立部分替身
$heartbeatService = $this->partialMock(HeartbeatService::class, function (MockInterface $mock) {
// 只模擬 updateCache 方法,其他方法保持原始實現
$mock->shouldReceive('updateCache')
->once()
->withArgs(function ($userId, $timestamp) {
return is_numeric($userId) && $timestamp instanceof \DateTime;
})
->andReturnTrue();
});

// 同時監聽 UserRepository(作為 Spy)
$userRepo = m::spy(UserRepository::class);
$this->app->instance(UserRepository::class, $userRepo);

// 執行測試 - 呼叫 API
$response = $this->actingAs($user)
->getJson('/api/heartbeat');

// 驗證 API 回應正確
$response->assertStatus(200)
->assertJsonStructure([
'status',
'message',
'last_seen_at'
]);

// 使用 Spy 驗證方法被調用,不需事先設定期望
$userRepo->shouldHaveReceived('updateLastSeen')
->once()
->with(m::type('int'), m::type('string'));
}
}

本例展示:

  • 部分替身(partialMock)保留大部分原始邏輯
  • Spy 模式監聽方法調用(不需事先設定)
  • 與 Laravel 測試輔助方法結合使用

常見錯誤與快速排解

  • 方法拼錯 / 參數不符Received … but expectations were not met
    • 檢查 shouldReceive 與實際呼叫是否一致。
  • 忘了關閉 Mock → 測試間干擾
    • Laravel 預設 Tests\TestCase 已在 tearDown() 自動 Mockery::close()
  • 過度 Mock 造成測試難維護
    • 只 Mock「跨界」依賴(I/O、第三方),核心邏輯依舊走真實物件。

Mockery × Laravel,測試不再被依賴綁架

  1. 環境已內建:Laravel 預裝 Mockery,零設定即可使用。
  2. 基本語法shouldReceive + once/times + withArgs
  3. 鏈式、Partial、Spy:對應實務各種複雜情境。

現在就挑一支服務,把真實依賴抽成介面並替換為 Mockery,看測試速度與可靠度能提升多少;相信你會在下一次 CI 跑綠燈的那一刻,徹底感受到「假物件帶來的真實快感」。

留言
avatar-img
留言分享你的想法!
avatar-img
詹姆士的軟體易開罐
26會員
86內容數
這是一系列以軟體開發為主題的輕鬆分享,內容涵蓋了技術選擇、開發經驗、實戰應用等多方面的議題。無論是如何在眾多框架中做出選擇,還是如何應對技術轉移的挑戰,這裡有幽默、有趣的對話風格,將複雜的技術問題轉化為易懂的故事。
2025/06/05
「登入之後,誰負責證明『我就是我』?又是誰負責記錄『我做了什麼』?」 當我們談認證(authentication)時,真正想要的其實是——可稽核的信任鏈。 我們從實務角度出發,帶你拆解 Laravel 11 裡 Sanctum 的兩種主流用法
Thumbnail
2025/06/05
「登入之後,誰負責證明『我就是我』?又是誰負責記錄『我做了什麼』?」 當我們談認證(authentication)時,真正想要的其實是——可稽核的信任鏈。 我們從實務角度出發,帶你拆解 Laravel 11 裡 Sanctum 的兩種主流用法
Thumbnail
2025/04/05
if寫得好,可以大大提高效率與可讀性。 Guard condition在函式起始先排除不合規輸入,能簡化結構、減少錯誤,使核心邏輯更聚焦並提高可維護性,也方便擴充與測試,在團隊協作和需求變動時,都能更快速應對。建議根據實際情況彈性運用,兼顧可讀性與維護成本。
Thumbnail
2025/04/05
if寫得好,可以大大提高效率與可讀性。 Guard condition在函式起始先排除不合規輸入,能簡化結構、減少錯誤,使核心邏輯更聚焦並提高可維護性,也方便擴充與測試,在團隊協作和需求變動時,都能更快速應對。建議根據實際情況彈性運用,兼顧可讀性與維護成本。
Thumbnail
2025/01/24
還記得我剛開始負責專案時,幾乎沒有人在意測試,改了程式碼就直接上線,結果小錯不斷、大災難頻傳。那種不知道哪裡會冒出 bug 的焦慮感,讓人每天都忙到焦頭爛額,卻依舊無從掌握系統品質。走過這段混亂的過程後,我才真正體會「為什麼需要測試」,也更明白「測試文化」並非只是技術細節。
Thumbnail
2025/01/24
還記得我剛開始負責專案時,幾乎沒有人在意測試,改了程式碼就直接上線,結果小錯不斷、大災難頻傳。那種不知道哪裡會冒出 bug 的焦慮感,讓人每天都忙到焦頭爛額,卻依舊無從掌握系統品質。走過這段混亂的過程後,我才真正體會「為什麼需要測試」,也更明白「測試文化」並非只是技術細節。
Thumbnail
看更多
你可能也想看
Thumbnail
沙龍一直是創作與交流的重要空間,這次 vocus 全面改版了沙龍介面,就是為了讓好內容被好好看見! 你可以自由編排你的沙龍首頁版位,新版手機介面也讓每位訪客都能更快找到感興趣的內容、成為你的支持者。 改版完成後可以在社群媒體分享新版面,並標記 @vocus.official⁠ ♥️ ⁠
Thumbnail
沙龍一直是創作與交流的重要空間,這次 vocus 全面改版了沙龍介面,就是為了讓好內容被好好看見! 你可以自由編排你的沙龍首頁版位,新版手機介面也讓每位訪客都能更快找到感興趣的內容、成為你的支持者。 改版完成後可以在社群媒體分享新版面,並標記 @vocus.official⁠ ♥️ ⁠
Thumbnail
每年4月、5月都是最多稅要繳的月份,當然大部份的人都是有機會繳到「綜合所得稅」,只是相當相當多人還不知道,原來繳給政府的稅!可以透過一些有活動的銀行信用卡或電子支付來繳,從繳費中賺一點點小確幸!就是賺個1%~2%大家也是很開心的,因為你們把沒回饋變成有回饋,就是用卡的最高境界 所得稅線上申報
Thumbnail
每年4月、5月都是最多稅要繳的月份,當然大部份的人都是有機會繳到「綜合所得稅」,只是相當相當多人還不知道,原來繳給政府的稅!可以透過一些有活動的銀行信用卡或電子支付來繳,從繳費中賺一點點小確幸!就是賺個1%~2%大家也是很開心的,因為你們把沒回饋變成有回饋,就是用卡的最高境界 所得稅線上申報
Thumbnail
CodeIgniter 3 和 Laravel 是兩種不同的 PHP 框架,各有其特點和適用場景。CodeIgniter 3 是一個輕量級框架,Laravel 是一個功能強大的現代 PHP 框架,同樣都有Models的它們有什麼樣的差別呢?
Thumbnail
CodeIgniter 3 和 Laravel 是兩種不同的 PHP 框架,各有其特點和適用場景。CodeIgniter 3 是一個輕量級框架,Laravel 是一個功能強大的現代 PHP 框架,同樣都有Models的它們有什麼樣的差別呢?
Thumbnail
先前提到 Quasar 的 Dialog Plugin 很好用,再讓我補充一個用法。
Thumbnail
先前提到 Quasar 的 Dialog Plugin 很好用,再讓我補充一個用法。
Thumbnail
在本章節中,我們探討了 PHP 中如何引用和管理套件。學習了如何使用 Composer 來安裝第三方套件,以及如何引用自定義模組。此外,我們還介紹了如何創建和使用自定義套件,並列舉了一些在 PHP 社群中常見且廣泛使用的套件和庫。通過掌握這些知識,開發者可以更有效地管理和利用各種資源。
Thumbnail
在本章節中,我們探討了 PHP 中如何引用和管理套件。學習了如何使用 Composer 來安裝第三方套件,以及如何引用自定義模組。此外,我們還介紹了如何創建和使用自定義套件,並列舉了一些在 PHP 社群中常見且廣泛使用的套件和庫。通過掌握這些知識,開發者可以更有效地管理和利用各種資源。
Thumbnail
※ 原本狀態:伺服器渲染 這是 MVC 架構下的 request / response 示意圖,在這張圖呈現的架構裡,畫面和資料都由同一個架構處理。 伺服器渲染流程: 瀏覽器針對特定網址送出請求。 路由器解析請求後,轉接給對應的 controller。 controller 按照要求,透過
Thumbnail
※ 原本狀態:伺服器渲染 這是 MVC 架構下的 request / response 示意圖,在這張圖呈現的架構裡,畫面和資料都由同一個架構處理。 伺服器渲染流程: 瀏覽器針對特定網址送出請求。 路由器解析請求後,轉接給對應的 controller。 controller 按照要求,透過
Thumbnail
當我們架好站、WebService測試完,接著就是測試區域網路連線啦~
Thumbnail
當我們架好站、WebService測試完,接著就是測試區域網路連線啦~
Thumbnail
前面已經安裝好IIS後,並且也新建站台了,那麼接下來這篇就會分享如何使用它
Thumbnail
前面已經安裝好IIS後,並且也新建站台了,那麼接下來這篇就會分享如何使用它
Thumbnail
戴夫寇爾研究團隊發現PHP在Windows系統上存在遠端程式碼執行漏洞,影響多個PHP版本,包括XAMPP預設安裝環境。漏洞源於字元編碼轉換的問題,允許攻擊者在遠端伺服器上執行任意程式碼。建議使用者立即升級至最新PHP版本,或採取臨時緩解措施。
Thumbnail
戴夫寇爾研究團隊發現PHP在Windows系統上存在遠端程式碼執行漏洞,影響多個PHP版本,包括XAMPP預設安裝環境。漏洞源於字元編碼轉換的問題,允許攻擊者在遠端伺服器上執行任意程式碼。建議使用者立即升級至最新PHP版本,或採取臨時緩解措施。
Thumbnail
代理模式通過封裝原始對象來實現對該對象的控制和管理,同時不改變原始對象的行為或客戶端與該對象互動的方式,以此介入或增強對該對象的訪問和操作。
Thumbnail
代理模式通過封裝原始對象來實現對該對象的控制和管理,同時不改變原始對象的行為或客戶端與該對象互動的方式,以此介入或增強對該對象的訪問和操作。
Thumbnail
當這產品的這個 API 被呼叫,再從回傳內容的某個欄位欄位來判斷,只要“這個欄位”顯示 false 就代表不支援」,雖然這樣的設計也能滿足功能需求…
Thumbnail
當這產品的這個 API 被呼叫,再從回傳內容的某個欄位欄位來判斷,只要“這個欄位”顯示 false 就代表不支援」,雖然這樣的設計也能滿足功能需求…
Thumbnail
有的時候,會希望在物件導向中對原生的Class新增功能的時候,大多我們都會寫一個新的class並繼承。 但是其實Laravel提供了一個不同的方式,讓我們可以在常用的Class上,直接新增想要的function,那就是macro。
Thumbnail
有的時候,會希望在物件導向中對原生的Class新增功能的時候,大多我們都會寫一個新的class並繼承。 但是其實Laravel提供了一個不同的方式,讓我們可以在常用的Class上,直接新增想要的function,那就是macro。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News