---
title: Mock vs Stub vs Fake vs Spy 完整指南 — Test Double 用對位置
description: Test Double 四種類型完整解釋。Mock / Stub / Fake / Spy 差別、用對位置、Playwright / pytest / Jest 實戰、第三方 API mock、WireMock / MSW / Pact 工具。
category: automation
tags: [mock, stub, fake, spy, test-double]
date: 2026-06-17
faq:
  - q: 面試常問 mock 跟 stub 差別、最簡答案？
    a: Stub = 給固定回應、不驗證互動；Mock = 給回應 + 驗證被怎麼呼叫。Mock 是 Stub 的超集。Fake = 簡化的真實實作（例如 in-memory DB）；Spy = wrap 真實物件、記錄呼叫但仍執行原邏輯。
  - q: 該用哪一個？
    a: 預設用 Stub（簡單）。需要驗證 collaborator 互動時用 Mock。重邏輯但 prod 用不起的（DB）用 Fake。想保留真實行為又要看互動用 Spy。
  - q: Mock 太多是反模式嗎？
    a: 是。過度 mock = 測試只驗實作細節不驗行為、refactor 一次全壞。原則：mock external boundary (API / DB)、別 mock 你自己的 code。
  - q: 第三方 API 一定要 mock？
    a: Integration test 用真實 staging（如果有）、Unit test 一律 mock。WireMock / MSW / Pact 是工具選擇。
---

# Mock / Stub / Fake / Spy — Test Double 完整指南

「我們的 test 都用 mock」聽起來合理、實際上**多數人把 Mock / Stub 混為一談、亂用一通**。這篇用 Martin Fowler 的標準分類講清楚。

## 四種 Test Double 一張圖

```mermaid
flowchart TD
    Double[Test Double] --> Stub[Stub<br>給固定回應]
    Double --> Mock[Mock<br>給回應 + 驗證呼叫]
    Double --> Fake[Fake<br>簡化的真實實作]
    Double --> Spy[Spy<br>wrap 真物件 + 記錄]
    Double --> Dummy[Dummy<br>佔位、不真用]

    Stub --> St1["用：取代慢/不可控的 collaborator<br>不在乎被怎麼呼叫"]
    Mock --> Mk1["用：驗證 A 確實有呼叫 B<br>行為驗證"]
    Fake --> Fk1["用：DB → in-memory<br>邏輯類似但快"]
    Spy --> Sp1["用：保留真實行為 + 記呼叫次數"]
    Dummy --> Dm1["用：constructor 要傳但不會用到"]

    style Stub fill:#06b6d4,color:#fff
    style Mock fill:#a855f7,color:#fff
    style Fake fill:#10b981,color:#fff
    style Spy fill:#f59e0b,color:#fff
```

## 對照表

| 類型 | 給回應 | 驗證呼叫 | 真實邏輯 | 何時用 |
|------|--------|---------|----------|--------|
| **Dummy** | ❌ | ❌ | ❌ | 不會被用、佔位 |
| **Stub** | ✓ 固定 | ❌ | ❌ | 取代外部、給可控資料 |
| **Spy** | ✓ 部分 | ✓ | ✓ | 保留行為、加觀察 |
| **Mock** | ✓ 可設 | ✓ 強制 | ❌ | 驗證互動 |
| **Fake** | ✓ 動態 | ❌ | ✓ 簡化 | 簡化的真實實作 |

## 範例：登入服務測試

### Stub — 給固定回應

```typescript
// LoginService 依賴 UserRepository
class LoginService {
  constructor(private repo: UserRepository) {}
  async login(email: string, password: string) {
    const user = await this.repo.findByEmail(email);
    return user?.password === password;
  }
}

// Stub：給固定回應、不在乎 findByEmail 被叫幾次
const userStub: UserRepository = {
  findByEmail: async () => ({ email: 'a@b.com', password: 'xxx' }),
};

const service = new LoginService(userStub);
const result = await service.login('a@b.com', 'xxx');
expect(result).toBe(true);
```

### Mock — 驗證呼叫

```typescript
const mockRepo = {
  findByEmail: jest.fn().mockResolvedValue({ email: 'a@b.com', password: 'xxx' }),
};

const service = new LoginService(mockRepo);
await service.login('a@b.com', 'xxx');

// 驗證 findByEmail 真的被呼叫、用對參數
expect(mockRepo.findByEmail).toHaveBeenCalledWith('a@b.com');
expect(mockRepo.findByEmail).toHaveBeenCalledTimes(1);
```

### Spy — 真行為 + 觀察

```typescript
const realRepo = new UserRepository(realDb);
const spy = jest.spyOn(realRepo, 'findByEmail');

const service = new LoginService(realRepo);
await service.login('a@b.com', 'xxx');

// 真的去查 DB、但也記錄了呼叫
expect(spy).toHaveBeenCalledWith('a@b.com');
```

### Fake — 簡化真實

```typescript
// In-memory 取代真 DB
class FakeUserRepository implements UserRepository {
  private users = new Map<string, User>();
  async findByEmail(email: string) {
    return this.users.get(email);
  }
  async save(user: User) {
    this.users.set(user.email, user);
  }
}

const fake = new FakeUserRepository();
await fake.save({ email: 'a@b.com', password: 'xxx' });

const service = new LoginService(fake);
const result = await service.login('a@b.com', 'xxx');
// 行為跟真 DB 一樣、但快且 isolated
```

## 何時用哪個

```mermaid
flowchart TD
    Q[要替換 collaborator?] --> Q1{要驗證<br>互動嗎?}
    Q1 -->|是| Mock[用 Mock]
    Q1 -->|否| Q2{需要真實<br>行為嗎?}
    Q2 -->|否| Stub[用 Stub]
    Q2 -->|要、但 prod 用不起| Fake[用 Fake]
    Q2 -->|要、且想加觀察| Spy[用 Spy]

    style Mock fill:#a855f7,color:#fff
    style Stub fill:#06b6d4,color:#fff
    style Fake fill:#10b981,color:#fff
    style Spy fill:#f59e0b,color:#fff
```

## QA 在 E2E / API 測試常用

### Playwright route mock (Stub)

```typescript
test('handle API error', async ({ page }) => {
  await page.route('**/api/users', route => {
    route.fulfill({
      status: 500,
      body: JSON.stringify({ error: 'Server Error' }),
    });
  });
  await page.goto('/users');
  await expect(page.getByText('系統忙碌')).toBeVisible();
});
```

### pytest + responses (Stub)

```python
import responses

@responses.activate
def test_payment_success():
    responses.add(
        responses.POST,
        "https://api.stripe.com/v1/charges",
        json={"id": "ch_123", "status": "succeeded"},
        status=200,
    )
    result = pay(amount=100)
    assert result["status"] == "succeeded"
```

### WireMock — 第三方 API mock server

```bash
docker run -d -p 8080:8080 wiremock/wiremock
```

```json
// mappings/stripe.json
{
  "request": {
    "method": "POST",
    "url": "/v1/charges"
  },
  "response": {
    "status": 200,
    "body": "{\"id\":\"ch_123\",\"status\":\"succeeded\"}"
  }
}
```

### MSW (Mock Service Worker) — 最現代

```typescript
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('https://api.example.com/users/:id', ({ params }) => {
    return HttpResponse.json({ id: params.id, name: 'Alice' });
  }),
];
```

**MSW 強項**：同一份 mock 給 Storybook / unit test / E2E 都用、超強 DX。

## Mock 反模式

```mermaid
flowchart TD
    Anti[Mock 反模式] --> A1["Mock everything 連自己 code 都 mock"]
    Anti --> A2["驗證 implementation detail<br>(例如 someInternalFunc 被叫 3 次)"]
    Anti --> A3["Mock 跟真實 API 不同步"]
    Anti --> A4["Stub 還在用過時的 response"]
    Anti --> A5["Test 全綠但 prod 全爆"]

    style A1 fill:#ef4444,color:#fff
    style A2 fill:#ef4444,color:#fff
    style A3 fill:#ef4444,color:#fff
    style A4 fill:#ef4444,color:#fff
    style A5 fill:#ef4444,color:#fff
```

### 原則：Mock 邊界、不 Mock 自己

```
✓ Mock 邊界（HTTP API / DB / 第三方）
✓ Mock 不可控（時間、隨機、UUID）
✗ Mock 自己 module 的 internal function
✗ Mock 純函式
```

## Contract Testing 補位

Mock 跟真實 API 不同步 → 用 [Pact / Contract Testing](/spec-review/microservices-contract-review.html) 強制兩邊對齊。

## 工具地圖

| 用途 | 工具 |
|------|------|
| HTTP request mock | nock (Node) / responses (Python) |
| Service Worker | MSW |
| 完整 API mock server | WireMock / Mockoon / Beeceptor |
| Test framework 內建 | jest.mock / pytest-mock |
| Contract testing | Pact / Spring Cloud Contract |
| DB Fake | SQLite in-memory / TinyDB |

## 面試常考

### Q: Mock 跟 Stub 差別？

**好答案**（30 秒）：
> Stub 給固定回應、不驗證互動 — 我設定它回什麼、test 就用。Mock 是 Stub 超集、額外驗證 collaborator 被怎麼呼叫 — 我除了設回應、還會 expect 它被呼叫過 N 次、用對參數。簡單講 Stub 是行為驗證、Mock 是互動驗證。

### Q: 你會在哪些情況不用 mock？

**好答案**：
> 純函式不用、自己 module 內部的 helper 不用。Integration test 階段我會用真實 DB（in-memory Fake）。E2E 我會 mock 第三方 API（Stripe sandbox 或 stub）但保留前後端真實串接。

## 給 QA 的 5 句

1. **預設用 Stub、需要驗證互動才升 Mock**
2. **Mock 邊界、不 Mock 自己 code**
3. **DB / 第三方 → Fake 或 contract test**
4. **MSW 是 2026 後 mock 首選**
5. **過度 mock 的 test 全綠但 prod 全爆**

## 最後

Mock / Stub / Fake / Spy 是 QA 跟 dev 共通語言。**用對名詞、用對位置 → 一週寫 case 順 5 倍**。從每天問自己「我這真的需要 Mock 還只是 Stub？」開始、半年後你會跟 dev 對話完全不同 level。

延伸：
- [Test Data Management](/automation/test-data-management.html)
- [API 測試實戰](/automation/api-testing-pytest.html)
- [Microservices Contract Review](/spec-review/microservices-contract-review.html)
