Test Data Management — Fixture / Factory / Seed / Cleanup 完整策略
「自動化測試跑兩個禮拜後 staging DB 變垃圾場」是 99% 團隊的痛。Test data 管理沒做好、自動化壽命 = 半年。這篇給你 6 種策略 + 完整實作。
為什麼 Test Data 是頭號殺手
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 策略
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(國家清單、產品分類)。
# tests/fixtures/users.yml
users:
- id: 1
email: [email protected]
role: admin
- id: 2
email: [email protected]
role: user
# 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
# 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"
# 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 也可以
// 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,
};
}
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。
# 跑一次塞滿基本資料
npm run db:seed
# 或
python manage.py seed_test_data
# 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 結構與分佈。
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。
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(產品目錄)。
@pytest.fixture(scope='session')
def all_countries(db):
# 整 session 只建一次
return CountryFactory.create_batch(50)
禁忌:可改寫的資料絕對不要 session scope。
命名規範(避免 prod data 跟 test data 混)
# ✅ 一眼看出是測試
email = f'qa-test-{uuid.uuid4()}@example.com'
name = f'QA_Test_User_{uuid.uuid4()}'
# ❌ 看起來像真用戶
email = '[email protected]'
name = 'Alice Chen'
好處:
- 後台搜
qa-test-一次 cleanup - Customer support 不會誤打給測試 email
- Analytics 可以排除測試流量
Cleanup 策略
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)
@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。
# 改成失敗時保留
@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(最乾淨)
@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 友善)
# 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
-- 每晚 3am 清測試 user
DELETE FROM users WHERE email LIKE 'qa-test-%' AND created_at < NOW() - INTERVAL '24 hours';
備胎策略。
跨環境的 Test Data 策略
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
規則:
- Local 用 seed + factory
- CI 用 fresh container
- Staging 用 anonymized prod snapshot
- Prod 絕對不寫測試資料
敏感資料處理
mindmap
root((敏感資料))
必去識別化
姓名
Email
手機
地址
身分證
信用卡
DOB
可用 fake
Faker library
Mockaroo
自建 generator
法規
GDPR
個資法
PCI-DSS
HIPAA
Faker 範例
from faker import Faker
fake = Faker('zh_TW')
email = fake.email() # [email protected]
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
反模式
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 件事
- Test data 規範寫進 onboarding(每個新 QA 進來都讀)
- Cleanup strategy 跟 dev 對齊(DB schema 改了 fixture 也要改)
- Test data dashboard — 多少測試 user / 還在 DB 多少(看健康)
給 QA 的 5 句
- 第一週做 test data 規範、後面省 6 個月維護
- 用 factory > fixture > snapshot > shared
- 命名加 prefix(qa-test-)救你的人生
- 失敗時別 cleanup、好用
- 永遠不要在 prod 寫 test data
最後
Test data 是自動化的隱形主角。寫得好沒人看見、寫不好整個 team 都受害。從今天起把所有 test data 加 qa-test- prefix、每個 test 自己的 factory、cleanup 寫進 fixture — 三個月後你的自動化會從「2 週後不能跑」變「3 年後還在跑」。