iT邦幫忙

2025 iThome 鐵人賽

DAY 25
0

昨天凌晨三點,手機響了。是值班同事:「購物車結帳功能掛了!」明明單元測試都通過,為什麼還是出問題?因為我們測試了每個零件,卻忘了測試它們組裝起來是否正常運作。這就是整合測試要解決的問題。

本日學習地圖 🗺️

前置基礎(Day 1-10)
├── 測試框架設置 ✅
├── 斷言與匹配器 ✅
├── TDD 循環 ✅
└── 測試組織 ✅

Kata 實戰(Day 11-17)
├── Roman Numerals ✅
├── 重構技巧 ✅
└── 進階 TDD ✅

框架特化(Day 18-27)
├── pytest 測試基礎 ✅
├── Mock 測試 ✅
├── HTTP 測試 ✅
├── 資料庫測試 ✅
├── CRUD 測試 ✅
├── 驗證測試 ✅
└── 整合測試 📍  <-- 我們在這裡!

為什麼需要整合測試? 🤔

想像你正在組裝一台電腦。每個零件(CPU、記憶體、硬碟)單獨測試都沒問題,但裝在一起卻開不了機。這就是只做單元測試的盲點。

單元測試 vs 整合測試

特性 單元測試 整合測試
範圍 單一函數/模組 多個元件協作
速度 快速 ⚡ 較慢 🐢
隔離性 完全隔離 部分真實環境
維護成本 中等
信心程度 局部信心 整體信心

今日實作:Todo App 整合測試 🏗️

讓我們為 Todo 應用寫一個完整的整合測試,測試從輸入到顯示的完整流程。

建立 tests/integration/conftest.py

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}"}

測試完整的 Todo CRUD 流程

建立 tests/integration/test_todo_workflow.py

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

測試錯誤處理 ⚠️

整合測試也要考慮異常情況:

建立 tests/integration/test_notification_integration.py

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()

測試資料一致性

建立 tests/integration/test_data_consistency.py

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

整合測試的層級 📊

建立 tests/integration/test_performance.py

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)

完整實作 效能整合測試

建立 tests/integration/test_error_recovery.py

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 加入以下整合測試:

  1. 批次操作測試:選擇多個 todos 並批次刪除
  2. 搜尋功能測試:輸入關鍵字過濾 todos
  3. 拖放排序測試:拖動 todo 項目改變順序

提示:整合測試要測試完整的用戶操作流程!

本日重點回顧 📝

今天我們學習了整合測試的重要概念:

  1. ✅ 理解整合測試的價值與定位
  2. ✅ 實作完整的用戶流程測試
  3. ✅ 處理異步操作與錯誤情況
  4. ✅ 平衡不同層級的測試

整合測試就像品管的最後一道關卡,確保所有零件組合後能正常運作。記住:好的整合測試能抓到單元測試漏掉的問題!

明日預告 🚀

明天我們將探討「效能測試」,學習如何確保應用的回應速度!

記住:整合測試是信心的來源,它證明你的程式真的能用! 💪


上一篇
Day 24 - 測試生命週期 Hook 🔄
下一篇
Day 26 - 效能測試 ⚡
系列文
Python pytest TDD 實戰:從零開始的測試驅動開發29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言