昨天我們學會了參數化測試,用優雅的方式處理大量測試資料。今天要解決一個新挑戰:「如何測試依賴外部服務的函數?」
想像一個場景:你的應用需要呼叫 API、寄送 email 或讀取資料庫。在測試時,你不希望真的去呼叫這些外部服務。今天我們要學習「測試替身」,了解如何用假物件替換真實依賴。
今天結束後,你將學會:
mock()
和 spy()
用法第一階段:打好基礎(Day 1-10)
├── Day 01 - 環境設置與第一個測試
├── Day 02 - 認識斷言(Assertions)
├── Day 03 - TDD 紅綠重構循環
├── Day 04 - 測試結構與組織
├── Day 05 - 測試生命週期
├── Day 06 - 參數化測試
├── Day 07 - 測試替身基礎 ★ 今天在這裡
├── ...
└── (更多精彩內容待續)
測試替身(Test Double)是在測試中用來替代真實依賴的假物件。主要有兩種:
// 問題:直接依賴外部服務
class UserService
{
public function getUserProfile(int $userId): array
{
$response = Http::get("/api/users/{$userId}"); // 真實 API 呼叫
$user = $response->json();
return [
'id' => $user['id'],
'name' => $user['name'],
'displayName' => strtoupper($user['name'])
];
}
}
測試時會遇到的問題:
Pest 提供了強大的 Mock 功能:
// 建立 Mock
$mockHttp = Http::fake([
'api/users/*' => Http::response([
'id' => 1,
'name' => 'John Doe',
'email' => 'john@example.com'
])
]);
建立 app/Services/UserService.php
:
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class UserService
{
public function getUserProfile(int $userId): array
{
$response = Http::get("/api/users/{$userId}");
$user = $response->json();
return [
'id' => $user['id'],
'name' => $user['name'],
'displayName' => strtoupper($user['name'])
];
}
public function createUser(array $userData): array
{
$response = Http::post('/api/users', $userData);
return $response->json();
}
}
建立 tests/Feature/Day07/UserServiceTest.php
:
<?php
use App\Services\UserService;
use Illuminate\Support\Facades\Http;
describe('UserService', function() {
beforeEach(function() {
$this->userService = new UserService();
});
it('gets user profile', function() {
// 建立 HTTP fake
Http::fake([
'/api/users/1' => Http::response([
'id' => 1,
'name' => 'John Doe',
'email' => 'john@example.com'
])
]);
$result = $this->userService->getUserProfile(1);
expect($result)->toEqual([
'id' => 1,
'name' => 'John Doe',
'displayName' => 'JOHN DOE'
]);
// 驗證 HTTP 請求被呼叫
Http::assertSent(function($request) {
return $request->url() === '/api/users/1';
});
});
it('creates user', function() {
Http::fake([
'/api/users' => Http::response(['id' => 2, 'name' => 'Jane'])
]);
$userData = ['name' => 'Jane', 'email' => 'jane@example.com'];
$result = $this->userService->createUser($userData);
expect($result)->toEqual(['id' => 2, 'name' => 'Jane']);
Http::assertSent(function($request) use ($userData) {
return $request->url() === '/api/users' &&
$request->data() === $userData;
});
});
});
it('handles api error', function() {
Http::fake([
'/api/users/999' => Http::response(null, 404)
]);
expect(function() {
$this->userService->getUserProfile(999);
})->toThrow(Exception::class);
});
it('tests with spy', function() {
$spy = $this->spy(UserService::class);
$spy->getUserProfile(1);
$spy->shouldHaveReceived('getUserProfile')->with(1)->once();
});
// ✅ 好的做法:清楚表達測試意圖
it('formats user name to uppercase', function() {
Http::fake([
'/api/users/1' => Http::response(['id' => 1, 'name' => 'john'])
]);
$result = $this->userService->getUserProfile(1);
expect($result['displayName'])->toBe('JOHN');
});
只 Mock 必要的部分,不要過度 Mock。
更新 tests/Feature/Day07/UserServiceTest.php
:
<?php
use App\Services\UserService;
use Illuminate\Support\Facades\Http;
describe('UserService with Test Doubles', function() {
beforeEach(function() {
$this->userService = new UserService();
});
describe('getUserProfile', function() {
it('returns formatted user data', function() {
Http::fake([
'/api/users/1' => Http::response([
'id' => 1,
'name' => 'John Doe',
'email' => 'john@example.com'
])
]);
$result = $this->userService->getUserProfile(1);
expect($result)->toHaveKeys(['id', 'name', 'displayName']);
expect($result['displayName'])->toBe('JOHN DOE');
Http::assertSent(fn($request) =>
str_contains($request->url(), '/api/users/1')
);
});
it('handles different user names correctly', function() {
Http::fake(['/api/users/*' => Http::response([
'id' => 2, 'name' => 'alice smith', 'email' => 'alice@example.com'
])]);
$result = $this->userService->getUserProfile(2);
expect($result['displayName'])->toBe('ALICE SMITH');
});
});
describe('createUser', function() {
it('sends correct data to api', function() {
Http::fake([
'/api/users' => Http::response(['id' => 3, 'success' => true])
]);
$userData = [
'name' => 'New User',
'email' => 'new@example.com'
];
$result = $this->userService->createUser($userData);
expect($result)->toHaveKey('success', true);
Http::assertSent(function($request) use ($userData) {
return $request->url() === '/api/users' &&
$request->data() === $userData;
});
});
});
});
今天我們深入學習了測試替身的重要概念:
測試替身是 TDD 中的重要工具,讓我們能夠:
記住:好的測試替身讓測試更快、更穩定、更專注。
明天我們將學習「例外處理測試」,了解如何驗證程式在異常情況下的行為。