---
title: Test Data Management — Fixture / Factory / Seed / Cleanup 完整策略
description: 自動化測試最大坑：test data 管理。Fixture vs Factory vs Mother、Seed 策略、cleanup 模式、跨環境資料、敏感資料處理。附 pytest / Playwright 範例。
category: automation
tags: [test-data, fixture, factory, seed, automation]
date: 2026-06-12
---

# Test Data Management — Fixture / Factory / Seed / Cleanup 完整策略

「自動化測試跑兩個禮拜後 staging DB 變垃圾場」是 99% 團隊的痛。**Test data 管理沒做好、自動化壽命 = 半年**。這篇給你 6 種策略 + 完整實作。

## 為什麼 Test Data 是頭號殺手

```mermaid
flowchart TD
    Auto[寫了自動化] --> P1[第 1 週<br>全綠]
    P1 --> P2[第 4 週<br>10% flaky]
    P2 --> P3[第 12 週<br>30% flaky + 重複帳號]
    P3 --> P4[第 24 週<br>沒人信任、棄置]

    P3 --> Why{為什麼?}
    Why --> R1[Test data 殘留]
    Why --> R2[Test 互打]
    Why --> R3[依賴順序]
    Why --> R4[資料污染]

    style Auto fill:#06b6d4,color:#fff
    style P4 fill:#ef4444,color:#fff
    style Why fill:#f59e0b,color:#fff
```

**Test data 不管理 = flaky test → 不信任 → 棄置**。

## 6 種 Test Data 策略

```mermaid
mindmap
  root((Test Data<br>Strategies))
    Fixture
      靜態 yaml/json
      適合穩定資料
      Setup 用
    Factory
      動態產生
      適合多變
      每 test 新建
    Seed Script
      DB 初始
      Migration 後跑
      適合 dev 環境
    Snapshot
      Prod 抽樣
      去識別化
      適合整合測試
    On-the-fly via API
      用真實 API 建
      最真實
      最慢
    Shared / Singleton
      只建一次
      慎用
      適合 read-only
```

每種有適合的場景。

## 策略 1: Fixture — 靜態檔案

**適合**：固定的 reference data（國家清單、產品分類）。

```yaml
# tests/fixtures/users.yml
users:
  - id: 1
    email: alice@test.com
    role: admin
  - id: 2
    email: bob@test.com
    role: user
```

```python
# pytest
@pytest.fixture
def users(scope="session"):
    with open('tests/fixtures/users.yml') as f:
        return yaml.safe_load(f)['users']

def test_admin_can_see_panel(users):
    admin = users[0]
    # ...
```

**問題**：多 test 共用 → 改一個壞一票。

## 策略 2: Factory — 動態產生（最強）

**適合**：每 test 新建獨立資料、避免共用衝突。

### Python 用 factory_boy

```python
# tests/factories.py
import factory
from app.models import User, Order

class UserFactory(factory.Factory):
    class Meta:
        model = User

    email = factory.Sequence(lambda n: f"qa-user-{n}@test.com")
    name = factory.Faker('name')
    role = "user"
    created_at = factory.LazyFunction(datetime.now)

class OrderFactory(factory.Factory):
    class Meta:
        model = Order

    user = factory.SubFactory(UserFactory)
    total = factory.Faker('pydecimal', positive=True, max_value=1000)
    status = "pending"
```

```python
# test
def test_user_can_view_own_order():
    user = UserFactory()
    order = OrderFactory(user=user)
    # ...

def test_admin_view_all_orders():
    # 一行建 10 個 user 各 3 個 order
    users = UserFactory.create_batch(10)
    for u in users:
        OrderFactory.create_batch(3, user=u)
```

### Playwright 也可以

```typescript
// fixtures/factories.ts
import { faker } from '@faker-js/faker';

let userIdSeq = 0;
export function makeUser(overrides = {}) {
  userIdSeq++;
  return {
    email: `qa-user-${userIdSeq}-${Date.now()}@test.com`,
    name: faker.person.fullName(),
    role: 'user',
    ...overrides,
  };
}

export function makeOrder(user, overrides = {}) {
  return {
    user_id: user.id,
    total: faker.number.float({ min: 10, max: 1000, precision: 0.01 }),
    status: 'pending',
    ...overrides,
  };
}
```

```typescript
test('user views own order', async ({ api, page }) => {
  const user = await api.post('/users', makeUser()).json();
  const order = await api.post('/orders', makeOrder(user)).json();
  // ...
});
```

**關鍵**：每個 test 自己的 user/data、不互打。

## 策略 3: Seed Script — DB 初始

**適合**：dev / staging 環境的 reference data + 少量 sample data。

```bash
# 跑一次塞滿基本資料
npm run db:seed
# 或
python manage.py seed_test_data
```

```python
# seeds/dev_seed.py
def seed():
    # Reference data
    Country.objects.bulk_create([
        Country(code='TW', name='台灣'),
        Country(code='JP', name='日本'),
    ])

    # Sample users
    for i in range(20):
        UserFactory(email=f'demo-{i}@example.com')

    # Sample orders
    OrderFactory.create_batch(100)
```

**問題**：Seed 只能跑一次、跑多次會重複。要設計成 idempotent。

## 策略 4: Snapshot — Prod 抽樣（謹慎用）

**適合**：整合測試需要真實 data 結構與分佈。

```mermaid
flowchart LR
    Prod[Production DB] --> Snap[Snapshot]
    Snap --> Anon[去識別化<br>name → fake<br>email → masked]
    Anon --> Staging[Staging DB]

    style Prod fill:#ef4444,color:#fff
    style Anon fill:#f59e0b,color:#fff
    style Staging fill:#10b981,color:#fff
```

### 必須去識別化的欄位

- ✅ 姓名 → faker
- ✅ Email → `user_{id}@masked.com`
- ✅ 手機 / 身分證 → fake
- ✅ 信用卡 → mask 中間 8 位
- ✅ 地址 → fake
- ✅ IP → 隨機

### 工具

- **pgreplay** (PostgreSQL)
- **AWS DMS**（雲端）
- 自建 script + Faker library

## 策略 5: On-the-fly via API — 最真實

**適合**：E2E test、要走真實 flow。

```typescript
test('register → verify email → login', async ({ page, api }) => {
  // 1. API 建 user (跟前端 register flow 一樣)
  const email = `qa-${Date.now()}@test.com`;
  await api.post('/auth/register', { email, password: 'Pass@123' });

  // 2. 從 mail server 拿 verification link
  const link = await getVerificationLink(email);

  // 3. 走 verify
  await page.goto(link);

  // 4. Login
  await page.goto('/login');
  await page.fill('[name=email]', email);
  await page.fill('[name=password]', 'Pass@123');
  await page.click('[type=submit]');

  // 5. Assert
  await expect(page).toHaveURL('/dashboard');
});
```

**最真實、最慢**。適合 critical flow。

## 策略 6: Shared / Singleton — 共用一份（慎用）

**適合**：read-only 的 fixture（產品目錄）。

```python
@pytest.fixture(scope='session')
def all_countries(db):
    # 整 session 只建一次
    return CountryFactory.create_batch(50)
```

**禁忌**：可改寫的資料絕對不要 session scope。

## 命名規範（避免 prod data 跟 test data 混）

```python
# ✅ 一眼看出是測試
email = f'qa-test-{uuid.uuid4()}@example.com'
name = f'QA_Test_User_{uuid.uuid4()}'

# ❌ 看起來像真用戶
email = 'alice@example.com'
name = 'Alice Chen'
```

**好處**：

1. 後台搜 `qa-test-` 一次 cleanup
2. Customer support 不會誤打給測試 email
3. Analytics 可以排除測試流量

## Cleanup 策略

```mermaid
flowchart TD
    Strategy{Cleanup<br>策略} --> S1[Test 內 cleanup<br>finally / afterEach]
    Strategy --> S2[Fixture teardown<br>pytest fixture]
    Strategy --> S3[每天 cron job<br>清過期測試資料]
    Strategy --> S4[整 DB reset<br>每次 CI 跑]
    Strategy --> S5[Transactional<br>每 test 一個 transaction、跑完 rollback]

    style S1 fill:#06b6d4,color:#fff
    style S2 fill:#10b981,color:#fff
    style S3 fill:#a855f7,color:#fff
    style S4 fill:#f59e0b,color:#fff
    style S5 fill:#ef4444,color:#fff
```

### Strategy A: 每 test cleanup（pytest fixture）

```python
@pytest.fixture
def fresh_user(api):
    user = api.post('/users', json=make_user()).json()
    yield user
    # Test 跑完自動清
    api.delete(f'/users/{user["id"]}')
```

**問題**：test fail 時 cleanup 也跑 → 看不到失敗瞬間的 data。

```python
# 改成失敗時保留
@pytest.fixture
def fresh_user(api, request):
    user = api.post('/users', json=make_user()).json()
    yield user
    if request.node.rep_call.passed:
        api.delete(f'/users/{user["id"]}')
    else:
        print(f"⚠️ Keeping user {user['id']} for debugging")
```

### Strategy B: Transactional（最乾淨）

```python
@pytest.fixture
def db_session():
    connection = engine.connect()
    transaction = connection.begin()
    session = Session(bind=connection)
    yield session
    session.close()
    transaction.rollback()  # 一切都不留
    connection.close()
```

**所有改動都 rollback**。最乾淨。**但 API 測試 / E2E 用不了**（API 走 HTTP、不在同 transaction）。

### Strategy C: 整 DB reset（CI 友善）

```bash
# CI 起 docker-compose 含 fresh DB
docker compose up -d db
python manage.py migrate
python manage.py seed_dev_data
npm run test:e2e
```

每次 CI 全新 DB → 0 污染。**慢一點但乾淨**。

### Strategy D: Nightly cron

```sql
-- 每晚 3am 清測試 user
DELETE FROM users WHERE email LIKE 'qa-test-%' AND created_at < NOW() - INTERVAL '24 hours';
```

備胎策略。

## 跨環境的 Test Data 策略

```mermaid
flowchart LR
    Env{環境} --> Local[Local Dev]
    Env --> CI[CI / Sandbox]
    Env --> Staging[Staging]
    Env --> Prod[Production]

    Local --> L1[Seed + Factory<br>盡量真實]
    CI --> C1[Fresh DB<br>每次 reset]
    Staging --> S1[Snapshot from prod<br>去識別化]
    Prod --> P1[❌ 不寫測試資料]

    style Prod fill:#ef4444,color:#fff
```

**規則**：

1. Local 用 seed + factory
2. CI 用 fresh container
3. Staging 用 anonymized prod snapshot
4. **Prod 絕對不寫測試資料**

## 敏感資料處理

```mermaid
mindmap
  root((敏感資料))
    必去識別化
      姓名
      Email
      手機
      地址
      身分證
      信用卡
      DOB
    可用 fake
      Faker library
      Mockaroo
      自建 generator
    法規
      GDPR
      個資法
      PCI-DSS
      HIPAA
```

### Faker 範例

```python
from faker import Faker
fake = Faker('zh_TW')

email = fake.email()        # alice@example.com
name = fake.name()          # 王小明
address = fake.address()    # 台北市信義區...
phone = fake.phone_number() # 0912345678
ssn = fake.ssn()           # 假身分證
cc = fake.credit_card_number()  # 4242 4242 4242 4242
```

## 測試卡號（信用卡測試專用）

```
Visa: 4111 1111 1111 1111
Visa (Stripe): 4242 4242 4242 4242
Mastercard: 5555 5555 5555 4444
Amex: 3782 822463 10005
過期卡: 4000 0000 0000 0069
扣款失敗: 4000 0000 0000 0002
3DS 必驗: 4000 0000 0000 3220
```

## 反模式

```mermaid
flowchart TD
    Anti[Test Data 反模式] --> A1["共用同一個 test user"]
    Anti --> A2["把真實 email 寫進 test code"]
    Anti --> A3["在 prod 跑 test"]
    Anti --> A4["從不 cleanup"]
    Anti --> A5["每 test 都 reset 整 DB"]
    Anti --> A6["test data 寫死、不能改"]
    Anti --> A7["敏感資料用真實的"]
    Anti --> A8["順序依賴：必須先跑 test A 才能跑 B"]

    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
    style A6 fill:#ef4444,color:#fff
    style A7 fill:#ef4444,color:#fff
    style A8 fill:#ef4444,color:#fff
```

## 工具地圖

| 工具 | 用途 | 語言 |
|------|------|------|
| **factory_boy** | Python factory | Python |
| **Faker** | 假資料 generator | Multi |
| **Mockaroo** | 線上產假資料 | Web |
| **fishery** | TS factory | TypeScript |
| **factories.ts** 手寫 | TS 手刻 | TypeScript |
| **Test Containers** | Docker 起測試 DB | Multi |
| **db-fixtures** | Django fixture | Python |
| **factory-girl** (TS) | TS factory | TypeScript |

## QA Lead 該推的 3 件事

1. **Test data 規範**寫進 onboarding（每個新 QA 進來都讀）
2. **Cleanup strategy** 跟 dev 對齊（DB schema 改了 fixture 也要改）
3. **Test data dashboard** — 多少測試 user / 還在 DB 多少（看健康）

## 給 QA 的 5 句

1. **第一週做 test data 規範、後面省 6 個月維護**
2. **用 factory > fixture > snapshot > shared**
3. **命名加 prefix（qa-test-）救你的人生**
4. **失敗時別 cleanup、好用**
5. **永遠不要在 prod 寫 test data**

## 最後

Test data 是自動化的隱形主角。**寫得好沒人看見、寫不好整個 team 都受害**。從今天起把所有 test data 加 `qa-test-` prefix、每個 test 自己的 factory、cleanup 寫進 fixture — 三個月後你的自動化會從「2 週後不能跑」變「3 年後還在跑」。
