昨天凌晨三點,手機響了。是值班同事:「購物車結帳功能掛了!」明明單元測試都通過,為什麼還是出問題?因為我們測試了每個零件,卻忘了測試它們組裝起來是否正常運作。這就是整合測試要解決的問題。
前置基礎(Day 1-10)
├── 測試框架設置 ✅
├── 斷言與匹配器 ✅
├── TDD 循環 ✅
└── 測試組織 ✅
Kata 實戰(Day 11-17)
├── Roman Numerals ✅
├── 重構技巧 ✅
└── 進階 TDD ✅
框架特化(Day 18-27)
├── pytest 測試基礎 ✅
├── Mock 測試 ✅
├── HTTP 測試 ✅
├── 資料庫測試 ✅
├── CRUD 測試 ✅
├── 驗證測試 ✅
└── 整合測試 📍 <-- 我們在這裡!
想像你正在組裝一台電腦。每個零件(CPU、記憶體、硬碟)單獨測試都沒問題,但裝在一起卻開不了機。這就是只做單元測試的盲點。
特性 | 單元測試 | 整合測試 |
---|---|---|
範圍 | 單一函數/模組 | 多個元件協作 |
速度 | 快速 ⚡ | 較慢 🐢 |
隔離性 | 完全隔離 | 部分真實環境 |
維護成本 | 低 | 中等 |
信心程度 | 局部信心 | 整體信心 |
讓我們為 Todo 應用寫一個完整的整合測試,測試從輸入到顯示的完整流程。
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.database import Base, get_db
@pytest.fixture
def test_db():
"""建立測試資料庫"""
engine = create_engine("sqlite:///:memory:")
SessionLocal = sessionmaker(bind=engine)
Base.metadata.create_all(bind=engine)
db = SessionLocal()
yield db
db.close()
Base.metadata.drop_all(bind=engine)
@pytest.fixture
def client(test_db):
"""建立測試客戶端"""
def override_get_db():
yield test_db
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as test_client:
yield test_client
app.dependency_overrides.clear()
@pytest.fixture
def auth_headers(client, test_db):
"""建立認證標頭"""
user_data = {"email": "test@example.com", "password": "test123"}
client.post("/auth/register", json=user_data)
response = client.post("/auth/login", json=user_data)
token = response.json()["access_token"]
return {"Authorization": f"Bearer {token}"}
class TestTodoWorkflow:
def test_complete_todo_lifecycle(self, client, auth_headers):
"""測試完整生命週期"""
# 建立
create_data = {"title": "Integration Test"}
response = client.post("/todos", json=create_data, headers=auth_headers)
assert response.status_code == 201
todo_id = response.json()["id"]
# 讀取
response = client.get(f"/todos/{todo_id}", headers=auth_headers)
assert response.status_code == 200
# 更新
update_data = {"completed": True}
response = client.patch(f"/todos/{todo_id}", json=update_data, headers=auth_headers)
assert response.status_code == 200
# 刪除
response = client.delete(f"/todos/{todo_id}", headers=auth_headers)
assert response.status_code == 204
def test_filter_and_pagination(self, client, auth_headers):
"""測試篩選和分頁"""
# 建立測試資料
for i in range(5):
data = {"title": f"Task {i}", "completed": i % 2 == 0}
client.post("/todos", json=data, headers=auth_headers)
# 測試篩選
response = client.get("/todos?completed=true", headers=auth_headers)
todos = response.json()
assert len(todos) == 3
整合測試也要考慮異常情況:
from unittest.mock import patch
class TestNotificationIntegration:
@patch('app.services.email.send_email')
def test_todo_reminder_sends_email(self, mock_email, client, auth_headers):
"""測試提醒郵件"""
todo_data = {"title": "Urgent Task", "reminder": True}
client.post("/todos", json=todo_data, headers=auth_headers)
response = client.post("/todos/check-reminders", headers=auth_headers)
assert response.status_code == 200
mock_email.assert_called_once()
class TestDataConsistency:
def test_cascade_delete(self, client, auth_headers):
"""測試級聯刪除"""
# 建立專案
response = client.post("/projects", json={"name": "Project"}, headers=auth_headers)
project_id = response.json()["id"]
# 建立關聯 Todos
for i in range(3):
client.post("/todos", json={"title": f"Task {i}", "project_id": project_id}, headers=auth_headers)
# 刪除專案應該刪除所有 Todos
client.delete(f"/projects/{project_id}", headers=auth_headers)
response = client.get("/todos", headers=auth_headers)
assert len(response.json()) == 0
import time
import concurrent.futures
class TestPerformance:
def test_concurrent_requests(self, client, auth_headers):
"""測試並發處理"""
def create_todo(i):
return client.post("/todos", json={"title": f"Todo {i}"}, headers=auth_headers)
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(create_todo, i) for i in range(20)]
results = [f.result() for f in concurrent.futures.as_completed(futures)]
assert all(r.status_code == 201 for r in results)
from unittest.mock import patch
class TestErrorRecovery:
def test_retry_mechanism(self, client, auth_headers):
"""測試重試機制"""
with patch('app.services.external_api.call') as mock_api:
mock_api.side_effect = [Exception("Timeout"), Exception("Error"), {"status": "ok"}]
response = client.post("/todos/sync-external", headers=auth_headers)
assert response.status_code == 200
assert mock_api.call_count == 3
試著實作一個購物車的整合測試,包含商品加入、數量調整、總價計算等完整流程。
請為你的 Todo App 加入以下整合測試:
提示:整合測試要測試完整的用戶操作流程!
今天我們學習了整合測試的重要概念:
整合測試就像品管的最後一道關卡,確保所有零件組合後能正常運作。記住:好的整合測試能抓到單元測試漏掉的問題!
明天我們將探討「效能測試」,學習如何確保應用的回應速度!
記住:整合測試是信心的來源,它證明你的程式真的能用! 💪