# 夥伴對齊系統 - 測試驅動開發文件 (TDD) ## 1. 文件資訊 **版本**: 1.0 **最後更新**: 2025年10月 **關聯文件**: SDD v1.0 **測試框架**: pytest + unittest --- ## 2. 測試策略概覽 ### 2.1 核心測試理念 **TDD循環**: ``` ┌──────────────────────────────────────┐ │ RED (紅燈) → GREEN (綠燈) → REFACTOR (重構) │ │ ↓ ↓ ↓ │ │ 寫測試失敗 實作最小程式碼 優化程式碼 │ └──────────────────────────────────────┘ ``` **測試金字塔**: ``` ┌─────────┐ │ E2E測試 │ 10% │ (5) │ ┌──┴─────────┴──┐ │ 整合測試 │ 30% │ (50) │ ┌──┴──────────────┴──┐ │ 單元測試 │ 60% │ (200+) │ └────────────────────┘ ``` ### 2.2 測試覆蓋率目標 | 測試類型 | 目標覆蓋率 | 優先級 | |---------|-----------|--------| | 單元測試 | 80%+ | 🔴 最高 | | 整合測試 | 70%+ | 🟡 高 | | API測試 | 100% | 🔴 最高 | | 端到端測試 | 核心流程100% | 🟡 高 | ### 2.3 測試環境配置 ```python # 測試環境設定檔 (test_config.py) TEST_DATABASE_URI = 'mysql://test_user:test_pass@localhost/test_db' TEST_SECRET_KEY = 'test_secret_key_for_testing_only' TESTING = True DEBUG = False SQLALCHEMY_ECHO = False # 關閉SQL日誌 ``` --- ## 3. 單元測試設計 ### 3.1 測試檔案結構 ``` tests/ ├── __init__.py ├── conftest.py # pytest配置與fixtures ├── unit/ # 單元測試 │ ├── __init__.py │ ├── test_models.py # 資料模型測試 │ ├── test_validators.py # 驗證邏輯測試 │ ├── test_utils.py # 工具函數測試 │ └── test_calculations.py # 計算邏輯測試 ├── integration/ # 整合測試 │ ├── __init__.py │ ├── test_api_assessments.py │ ├── test_api_star.py │ ├── test_api_rankings.py │ └── test_database.py ├── e2e/ # 端到端測試 │ ├── __init__.py │ └── test_workflows.py └── fixtures/ # 測試資料 ├── sample_assessments.json └── sample_star_feedbacks.json ``` ### 3.2 測試基礎設施 (conftest.py) ```python import pytest from app import create_app, db from app.models import Assessment, Capability, StarFeedback, EmployeePoints @pytest.fixture(scope='session') def app(): """建立測試用Flask應用程式""" app = create_app('testing') return app @pytest.fixture(scope='session') def _db(app): """建立測試資料庫""" with app.app_context(): db.create_all() yield db db.drop_all() @pytest.fixture(scope='function') def client(app): """建立測試客戶端""" return app.test_client() @pytest.fixture(scope='function') def db_session(_db): """提供獨立的資料庫session""" connection = _db.engine.connect() transaction = connection.begin() session = _db.create_scoped_session( options={"bind": connection, "binds": {}} ) _db.session = session yield session transaction.rollback() connection.close() session.remove() @pytest.fixture def sample_capability(): """範例能力項目""" return Capability( name="程式設計與開發", l1_description="基礎程式撰寫能力", l2_description="獨立完成功能開發", l3_description="架構設計與優化", l4_description="技術領導與指導", l5_description="技術策略制定" ) @pytest.fixture def sample_assessment_data(): """範例評估資料""" return { "department": "技術部", "position": "資深工程師", "employee_name": "測試員工A", "assessment_data": { "L1": ["程式設計與開發"], "L2": ["系統分析與設計"], "L3": ["專案管理"], "L4": [], "L5": [] } } @pytest.fixture def sample_star_feedback(): """範例STAR回饋""" return { "evaluator_name": "主管A", "evaluatee_name": "員工B", "evaluatee_department": "技術部", "evaluatee_position": "工程師", "situation": "專案遇到緊急技術問題,系統出現嚴重效能瓶頸", "task": "需在24小時內找出問題根源並提出解決方案", "action": "透過profiling工具分析,發現資料庫查詢N+1問題,重構查詢邏輯", "result": "系統回應時間從3秒降至0.5秒,客戶滿意度提升", "score": 4, "feedback_date": "2025-10-15" } ``` --- ## 4. 資料模型測試 ### 4.1 Assessment Model 測試 **測試檔案**: `tests/unit/test_models.py` #### 測試案例 1: 建立評估記錄 ```python class TestAssessmentModel: """Assessment模型測試""" def test_create_assessment_success(self, db_session, sample_assessment_data): """ 測試目標: 驗證評估記錄能正確建立 測試步驟: 1. 建立Assessment物件 2. 儲存至資料庫 3. 驗證資料正確性 預期結果: - 記錄成功建立 - 自動生成ID - created_at自動設定 - assessment_data正確儲存為JSON """ # RED: 先寫測試 assessment = Assessment( department=sample_assessment_data['department'], position=sample_assessment_data['position'], employee_name=sample_assessment_data['employee_name'], assessment_data=sample_assessment_data['assessment_data'] ) db_session.add(assessment) db_session.commit() # 驗證 assert assessment.id is not None assert assessment.department == "技術部" assert assessment.position == "資深工程師" assert assessment.created_at is not None assert isinstance(assessment.assessment_data, dict) assert "L1" in assessment.assessment_data def test_assessment_requires_department(self, db_session): """ 測試目標: 驗證必填欄位驗證 預期結果: 缺少department時拋出IntegrityError """ from sqlalchemy.exc import IntegrityError assessment = Assessment( position="工程師", assessment_data={} ) db_session.add(assessment) with pytest.raises(IntegrityError): db_session.commit() def test_assessment_json_serialization(self, db_session, sample_assessment_data): """ 測試目標: 驗證JSON序列化/反序列化 預期結果: assessment_data能正確轉換為JSON並還原 """ assessment = Assessment(**sample_assessment_data) db_session.add(assessment) db_session.commit() # 重新查詢 retrieved = db_session.query(Assessment).filter_by( id=assessment.id ).first() assert retrieved.assessment_data == sample_assessment_data['assessment_data'] assert isinstance(retrieved.assessment_data['L1'], list) ``` #### 測試案例 2: 查詢與篩選 ```python def test_filter_by_department(self, db_session): """ 測試目標: 驗證按部門篩選功能 測試資料: - 技術部: 3筆 - 業務部: 2筆 預期結果: 正確篩選出對應部門的資料 """ # 建立測試資料 departments = ["技術部"] * 3 + ["業務部"] * 2 for i, dept in enumerate(departments): assessment = Assessment( department=dept, position="職位" + str(i), assessment_data={} ) db_session.add(assessment) db_session.commit() # 測試篩選 tech_assessments = db_session.query(Assessment).filter_by( department="技術部" ).all() assert len(tech_assessments) == 3 assert all(a.department == "技術部" for a in tech_assessments) def test_pagination(self, db_session): """ 測試目標: 驗證分頁功能 測試資料: 25筆記錄 分頁設定: 每頁10筆 預期結果: - 第1頁: 10筆 - 第2頁: 10筆 - 第3頁: 5筆 """ # 建立25筆測試資料 for i in range(25): assessment = Assessment( department="部門" + str(i % 3), position="職位", assessment_data={} ) db_session.add(assessment) db_session.commit() # 測試分頁 page1 = db_session.query(Assessment).limit(10).offset(0).all() page2 = db_session.query(Assessment).limit(10).offset(10).all() page3 = db_session.query(Assessment).limit(10).offset(20).all() assert len(page1) == 10 assert len(page2) == 10 assert len(page3) == 5 ``` ### 4.2 StarFeedback Model 測試 ```python class TestStarFeedbackModel: """STAR回饋模型測試""" def test_create_star_feedback_success(self, db_session, sample_star_feedback): """ 測試目標: 驗證STAR回饋記錄建立 預期結果: - 所有欄位正確儲存 - points_earned自動計算 (score * 10) """ feedback = StarFeedback(**sample_star_feedback) db_session.add(feedback) db_session.commit() assert feedback.id is not None assert feedback.score == 4 assert feedback.points_earned == 40 # 自動計算 assert feedback.evaluator_name == "主管A" assert len(feedback.situation) > 0 def test_score_validation(self, db_session): """ 測試目標: 驗證評分範圍限制 測試案例: - score = 0: 應失敗 - score = 1-5: 應成功 - score = 6: 應失敗 預期結果: 只接受1-5的評分 """ from sqlalchemy.exc import IntegrityError # 測試無效評分 (0) invalid_feedback = StarFeedback( evaluator_name="主管", evaluatee_name="員工", evaluatee_department="部門", evaluatee_position="職位", situation="S", task="T", action="A", result="R", score=0, feedback_date="2025-10-15" ) db_session.add(invalid_feedback) with pytest.raises(IntegrityError): db_session.commit() db_session.rollback() # 測試有效評分 (1-5) for score in range(1, 6): valid_feedback = StarFeedback( evaluator_name="主管", evaluatee_name=f"員工{score}", evaluatee_department="部門", evaluatee_position="職位", situation="S", task="T", action="A", result="R", score=score, feedback_date="2025-10-15" ) db_session.add(valid_feedback) db_session.commit() assert valid_feedback.points_earned == score * 10 def test_star_required_fields(self, db_session): """ 測試目標: 驗證STAR四個欄位為必填 預期結果: 缺少任一STAR欄位時應失敗 """ from sqlalchemy.exc import IntegrityError base_data = { "evaluator_name": "主管", "evaluatee_name": "員工", "evaluatee_department": "部門", "evaluatee_position": "職位", "score": 3, "feedback_date": "2025-10-15" } # 測試缺少每個STAR欄位 star_fields = ['situation', 'task', 'action', 'result'] for missing_field in star_fields: data = base_data.copy() # 給予其他三個欄位值 for field in star_fields: if field != missing_field: data[field] = "測試內容" feedback = StarFeedback(**data) db_session.add(feedback) with pytest.raises(IntegrityError): db_session.commit() db_session.rollback() ``` ### 4.3 EmployeePoints Model 測試 ```python class TestEmployeePointsModel: """員工積分模型測試""" def test_create_employee_points(self, db_session): """ 測試目標: 建立員工積分記錄 預期結果: 預設積分為0 """ employee = EmployeePoints( employee_name="員工A", department="技術部", position="工程師" ) db_session.add(employee) db_session.commit() assert employee.total_points == 0 assert employee.monthly_points == 0 assert employee.last_updated is not None def test_update_points(self, db_session): """ 測試目標: 驗證積分更新邏輯 測試步驟: 1. 建立員工記錄 2. 新增30積分 3. 再新增20積分 預期結果: - total_points = 50 - monthly_points = 50 - last_updated已更新 """ employee = EmployeePoints( employee_name="員工B", department="技術部", position="工程師" ) db_session.add(employee) db_session.commit() original_time = employee.last_updated # 第一次新增積分 employee.total_points += 30 employee.monthly_points += 30 db_session.commit() assert employee.total_points == 30 assert employee.last_updated > original_time # 第二次新增積分 employee.total_points += 20 employee.monthly_points += 20 db_session.commit() assert employee.total_points == 50 assert employee.monthly_points == 50 def test_unique_employee_name(self, db_session): """ 測試目標: 驗證員工姓名唯一性約束 預期結果: 重複員工姓名應失敗 """ from sqlalchemy.exc import IntegrityError # 第一個員工 employee1 = EmployeePoints( employee_name="重複姓名", department="技術部", position="工程師" ) db_session.add(employee1) db_session.commit() # 嘗試建立同名員工 employee2 = EmployeePoints( employee_name="重複姓名", department="業務部", position="專員" ) db_session.add(employee2) with pytest.raises(IntegrityError): db_session.commit() ``` --- ## 5. 業務邏輯測試 ### 5.1 積分計算邏輯測試 **測試檔案**: `tests/unit/test_calculations.py` ```python class TestPointsCalculation: """積分計算邏輯測試""" @pytest.mark.parametrize("score,expected_points", [ (1, 10), (2, 20), (3, 30), (4, 40), (5, 50), ]) def test_calculate_points_from_score(self, score, expected_points): """ 測試目標: 驗證積分計算公式 計算規則: points = score × 10 測試案例: 所有有效評分 (1-5) """ from app.utils import calculate_points points = calculate_points(score) assert points == expected_points def test_invalid_score_raises_error(self): """ 測試目標: 驗證無效評分處理 測試案例: - score < 1 - score > 5 - score 非整數 預期結果: 拋出ValueError """ from app.utils import calculate_points with pytest.raises(ValueError): calculate_points(0) with pytest.raises(ValueError): calculate_points(6) with pytest.raises(ValueError): calculate_points(3.5) def test_accumulate_employee_points(self, db_session): """ 測試目標: 驗證員工積分累積邏輯 測試情境: - 員工收到3次回饋: 評分3, 4, 5 - 預期總積分: 120分 預期結果: - total_points正確累加 - monthly_points正確累加 """ from app.services import accumulate_points employee = EmployeePoints( employee_name="測試員工", department="技術部", position="工程師" ) db_session.add(employee) db_session.commit() # 累加積分 scores = [3, 4, 5] for score in scores: points = calculate_points(score) accumulate_points(db_session, "測試員工", points) # 驗證 updated_employee = db_session.query(EmployeePoints).filter_by( employee_name="測試員工" ).first() assert updated_employee.total_points == 120 assert updated_employee.monthly_points == 120 ``` ### 5.2 排名計算邏輯測試 ```python class TestRankingCalculation: """排名計算邏輯測試""" def test_calculate_rankings_basic(self, db_session): """ 測試目標: 基本排名計算 測試資料: - 員工A: 100分 → 排名1 - 員工B: 80分 → 排名2 - 員工C: 60分 → 排名3 預期結果: 排名正確且按積分降序 """ from app.services import calculate_monthly_rankings from datetime import date # 建立測試資料 employees = [ EmployeePoints(employee_name="員工A", department="技術部", position="工程師", total_points=100, monthly_points=100), EmployeePoints(employee_name="員工B", department="技術部", position="工程師", total_points=80, monthly_points=80), EmployeePoints(employee_name="員工C", department="業務部", position="專員", total_points=60, monthly_points=60), ] for emp in employees: db_session.add(emp) db_session.commit() # 計算排名 ranking_month = date(2025, 10, 1) calculate_monthly_rankings(db_session, ranking_month) # 驗證排名 rankings = db_session.query(MonthlyRanking).filter_by( ranking_month=ranking_month ).order_by(MonthlyRanking.ranking).all() assert len(rankings) == 3 assert rankings[0].employee_name == "員工A" assert rankings[0].ranking == 1 assert rankings[1].employee_name == "員工B" assert rankings[1].ranking == 2 assert rankings[2].employee_name == "員工C" assert rankings[2].ranking == 3 def test_calculate_rankings_with_ties(self, db_session): """ 測試目標: 並列排名處理 測試資料: - 員工A: 100分 → 排名1 - 員工B: 80分 → 排名2 - 員工C: 80分 → 排名2 (並列) - 員工D: 60分 → 排名4 (跳號) 預期結果: 並列者同排名,下一名次跳號 """ from app.services import calculate_monthly_rankings from datetime import date employees = [ EmployeePoints(employee_name="員工A", department="技術部", position="工程師", total_points=100, monthly_points=100), EmployeePoints(employee_name="員工B", department="技術部", position="工程師", total_points=80, monthly_points=80), EmployeePoints(employee_name="員工C", department="業務部", position="專員", total_points=80, monthly_points=80), EmployeePoints(employee_name="員工D", department="業務部", position="專員", total_points=60, monthly_points=60), ] for emp in employees: db_session.add(emp) db_session.commit() ranking_month = date(2025, 10, 1) calculate_monthly_rankings(db_session, ranking_month) rankings = db_session.query(MonthlyRanking).filter_by( ranking_month=ranking_month ).order_by(MonthlyRanking.ranking, MonthlyRanking.employee_name).all() assert rankings[0].ranking == 1 assert rankings[1].ranking == 2 assert rankings[2].ranking == 2 # 並列 assert rankings[3].ranking == 4 # 跳號 def test_exclude_zero_points_from_ranking(self, db_session): """ 測試目標: 零積分員工不列入排名 測試資料: - 員工A: 50分 - 員工B: 0分 預期結果: 只有員工A出現在排名中 """ from app.services import calculate_monthly_rankings from datetime import date employees = [ EmployeePoints(employee_name="員工A", department="技術部", position="工程師", total_points=50, monthly_points=50), EmployeePoints(employee_name="員工B", department="技術部", position="工程師", total_points=0, monthly_points=0), ] for emp in employees: db_session.add(emp) db_session.commit() ranking_month = date(2025, 10, 1) calculate_monthly_rankings(db_session, ranking_month) rankings = db_session.query(MonthlyRanking).filter_by( ranking_month=ranking_month ).all() assert len(rankings) == 1 assert rankings[0].employee_name == "員工A" def test_reset_monthly_points_after_calculation(self, db_session): """ 測試目標: 計算排名後重置月度積分 預期結果: - total_points保持不變 - monthly_points重置為0 """ from app.services import calculate_monthly_rankings from datetime import date employee = EmployeePoints( employee_name="員工A", department="技術部", position="工程師", total_points=100, monthly_points=100 ) db_session.add(employee) db_session.commit() ranking_month = date(2025, 10, 1) calculate_monthly_rankings(db_session, ranking_month) updated_employee = db_session.query(EmployeePoints).filter_by( employee_name="員工A" ).first() assert updated_employee.total_points == 100 # 不變 assert updated_employee.monthly_points == 0 # 重置 ``` ### 5.3 資料驗證測試 ```python class TestDataValidation: """資料驗證邏輯測試""" def test_validate_assessment_data_valid(self): """ 測試目標: 驗證有效的評估資料 預期結果: 驗證通過,回傳True """ from app.validators import validate_assessment_data valid_data = { "department": "技術部", "position": "工程師", "assessment_data": { "L1": ["能力1"], "L2": ["能力2"], "L3": [], "L4": [], "L5": [] } } is_valid, errors = validate_assessment_data(valid_data) assert is_valid is True assert len(errors) == 0 def test_validate_assessment_data_missing_required_fields(self): """ 測試目標: 驗證缺少必填欄位 測試案例: - 缺少department - 缺少position - 缺少assessment_data 預期結果: 驗證失敗,回傳錯誤訊息 """ from app.validators import validate_assessment_data # 缺少department data1 = { "position": "工程師", "assessment_data": {"L1": []} } is_valid1, errors1 = validate_assessment_data(data1) assert is_valid1 is False assert "department" in str(errors1).lower() # 缺少assessment_data data2 = { "department": "技術部", "position": "工程師" } is_valid2, errors2 = validate_assessment_data(data2) assert is_valid2 is False assert "assessment_data" in str(errors2).lower() def test_validate_star_feedback_valid(self): """ 測試目標: 驗證有效的STAR回饋 預期結果: 所有必填欄位驗證通過 """ from app.validators import validate_star_feedback valid_feedback = { "evaluator_name": "主管A", "evaluatee_name": "員工B", "evaluatee_department": "技術部", "evaluatee_position": "工程師", "situation": "專案遇到緊急問題需要處理" * 2, # > 10字 "task": "需要在限定時間內解決問題" * 2, "action": "採取了具體的行動步驟處理" * 2, "result": "成功解決問題並達成目標" * 2, "score": 4, "feedback_date": "2025-10-15" } is_valid, errors = validate_star_feedback(valid_feedback) assert is_valid is True assert len(errors) == 0 def test_validate_star_content_minimum_length(self): """ 測試目標: 驗證STAR內容最小長度要求 要求: 每個STAR欄位至少10字元 預期結果: 少於10字元應驗證失敗 """ from app.validators import validate_star_feedback short_feedback = { "evaluator_name": "主管A", "evaluatee_name": "員工B", "evaluatee_department": "技術部", "evaluatee_position": "工程師", "situation": "太短", # < 10字 "task": "需要在限定時間內解決問題" * 2, "action": "採取了具體的行動步驟處理" * 2, "result": "成功解決問題並達成目標" * 2, "score": 4, "feedback_date": "2025-10-15" } is_valid, errors = validate_star_feedback(short_feedback) assert is_valid is False assert "situation" in str(errors).lower() assert "10" in str(errors) def test_validate_score_range(self): """ 測試目標: 驗證評分範圍 有效範圍: 1-5 測試案例: - score = 0: 失敗 - score = 3: 成功 - score = 6: 失敗 - score = "abc": 失敗 """ from app.validators import validate_score assert validate_score(0)[0] is False assert validate_score(1)[0] is True assert validate_score(3)[0] is True assert validate_score(5)[0] is True assert validate_score(6)[0] is False assert validate_score("abc")[0] is False def test_validate_feedback_date_not_future(self): """ 測試目標: 驗證回饋日期不可為未來日期 預期結果: 未來日期應驗證失敗 """ from app.validators import validate_feedback_date from datetime import date, timedelta today = date.today() yesterday = today - timedelta(days=1) tomorrow = today + timedelta(days=1) assert validate_feedback_date(yesterday)[0] is True assert validate_feedback_date(today)[0] is True assert validate_feedback_date(tomorrow)[0] is False ``` --- ## 6. API整合測試 ### 6.1 Assessment API 測試 **測試檔案**: `tests/integration/test_api_assessments.py` ```python class TestAssessmentAPI: """能力評估API整合測試""" def test_get_capabilities_success(self, client, db_session, sample_capability): """ 測試目標: GET /api/capabilities 測試步驟: 1. 建立測試能力項目 2. 呼叫API 3. 驗證回應 預期結果: - 狀態碼: 200 - 回傳所有啟用的能力項目 - JSON格式正確 """ # 準備資料 db_session.add(sample_capability) db_session.commit() # 呼叫API response = client.get('/api/capabilities') # 驗證 assert response.status_code == 200 data = response.get_json() assert 'capabilities' in data assert len(data['capabilities']) > 0 capability = data['capabilities'][0] assert 'id' in capability assert 'name' in capability assert 'l1_description' in capability def test_post_assessment_success(self, client, db_session, sample_assessment_data): """ 測試目標: POST /api/assessments 測試步驟: 1. 提交有效的評估資料 2. 驗證回應 3. 確認資料已儲存至資料庫 預期結果: - 狀態碼: 201 - 回傳成功訊息 - 資料庫有新記錄 """ response = client.post( '/api/assessments', json=sample_assessment_data, content_type='application/json' ) # 驗證回應 assert response.status_code == 201 data = response.get_json() assert data['success'] is True assert 'assessment_id' in data assert data['message'] == '評估提交成功' # 驗證資料庫 assessment = db_session.query(Assessment).filter_by( id=data['assessment_id'] ).first() assert assessment is not None assert assessment.department == "技術部" assert assessment.position == "資深工程師" def test_post_assessment_missing_required_field(self, client): """ 測試目標: POST /api/assessments with missing field 測試案例: 缺少必填欄位department 預期結果: - 狀態碼: 400 - 錯誤訊息明確 """ invalid_data = { "position": "工程師", "assessment_data": {"L1": []} } response = client.post( '/api/assessments', json=invalid_data, content_type='application/json' ) assert response.status_code == 400 data = response.get_json() assert data['success'] is False assert 'department' in data['message'].lower() def test_post_assessment_invalid_json(self, client): """ 測試目標: POST /api/assessments with invalid JSON 預期結果: - 狀態碼: 400 - 錯誤訊息 """ response = client.post( '/api/assessments', data='invalid json string', content_type='application/json' ) assert response.status_code == 400 def test_get_assessments_with_pagination(self, client, db_session): """ 測試目標: GET /api/assessments with pagination 測試資料: 25筆評估記錄 請求參數: page=2, per_page=10 預期結果: - 狀態碼: 200 - 回傳第2頁的10筆資料 - 包含分頁資訊 """ # 建立25筆測試資料 for i in range(25): assessment = Assessment( department="部門" + str(i), position="職位" + str(i), assessment_data={} ) db_session.add(assessment) db_session.commit() # 請求第2頁 response = client.get('/api/assessments?page=2&per_page=10') assert response.status_code == 200 data = response.get_json() assert len(data['assessments']) == 10 assert data['page'] == 2 assert data['per_page'] == 10 assert data['total'] == 25 assert data['pages'] == 3 def test_get_assessments_with_filters(self, client, db_session): """ 測試目標: GET /api/assessments with filters 測試資料: - 技術部-工程師: 3筆 - 技術部-專員: 2筆 - 業務部-工程師: 2筆 篩選條件: department=技術部, position=工程師 預期結果: 只回傳3筆符合條件的資料 """ test_data = [ ("技術部", "工程師"), ("技術部", "工程師"), ("技術部", "工程師"), ("技術部", "專員"), ("技術部", "專員"), ("業務部", "工程師"), ("業務部", "工程師"), ] for dept, pos in test_data: assessment = Assessment( department=dept, position=pos, assessment_data={} ) db_session.add(assessment) db_session.commit() response = client.get('/api/assessments?department=技術部&position=工程師') assert response.status_code == 200 data = response.get_json() assert data['total'] == 3 # 驗證所有資料都符合篩選條件 for assessment in data['assessments']: assert assessment['department'] == '技術部' assert assessment['position'] == '工程師' ``` ### 6.2 STAR Feedback API 測試 ```python class TestStarFeedbackAPI: """STAR回饋API整合測試""" def test_post_star_feedback_success(self, client, db_session, sample_star_feedback): """ 測試目標: POST /api/star-feedbacks 測試步驟: 1. 提交有效的STAR回饋 2. 驗證回應 3. 確認積分已更新 預期結果: - 狀態碼: 201 - 回傳成功訊息與積分 - employee_points表已更新 """ response = client.post( '/api/star-feedbacks', json=sample_star_feedback, content_type='application/json' ) # 驗證回應 assert response.status_code == 201 data = response.get_json() assert data['success'] is True assert data['points_earned'] == 40 # score 4 * 10 # 驗證資料庫 - 回饋記錄 feedback = db_session.query(StarFeedback).filter_by( evaluatee_name="員工B" ).first() assert feedback is not None assert feedback.score == 4 assert feedback.points_earned == 40 # 驗證資料庫 - 積分更新 employee = db_session.query(EmployeePoints).filter_by( employee_name="員工B" ).first() assert employee is not None assert employee.total_points == 40 assert employee.monthly_points == 40 def test_post_star_feedback_accumulate_points(self, client, db_session): """ 測試目標: 多次回饋的積分累積 測試步驟: 1. 提交第一次回饋(評分3) 2. 提交第二次回饋(評分5) 3. 驗證積分正確累加 預期結果: total_points = 80 (30 + 50) """ feedback1 = { "evaluator_name": "主管A", "evaluatee_name": "員工C", "evaluatee_department": "技術部", "evaluatee_position": "工程師", "situation": "情境描述內容足夠長度測試", "task": "任務描述內容足夠長度測試", "action": "行動描述內容足夠長度測試", "result": "結果描述內容足夠長度測試", "score": 3, "feedback_date": "2025-10-15" } feedback2 = feedback1.copy() feedback2["score"] = 5 feedback2["evaluator_name"] = "主管B" # 第一次回饋 response1 = client.post('/api/star-feedbacks', json=feedback1) assert response1.status_code == 201 # 第二次回饋 response2 = client.post('/api/star-feedbacks', json=feedback2) assert response2.status_code == 201 # 驗證積分累積 employee = db_session.query(EmployeePoints).filter_by( employee_name="員工C" ).first() assert employee.total_points == 80 assert employee.monthly_points == 80 def test_post_star_feedback_invalid_score(self, client, sample_star_feedback): """ 測試目標: 無效評分處理 測試案例: score = 6 (超出範圍) 預期結果: - 狀態碼: 400 - 錯誤訊息 """ invalid_feedback = sample_star_feedback.copy() invalid_feedback["score"] = 6 response = client.post('/api/star-feedbacks', json=invalid_feedback) assert response.status_code == 400 data = response.get_json() assert data['success'] is False assert 'score' in data['message'].lower() def test_post_star_feedback_short_content(self, client, sample_star_feedback): """ 測試目標: STAR內容長度驗證 測試案例: situation欄位少於10字元 預期結果: - 狀態碼: 400 - 錯誤訊息指出內容太短 """ invalid_feedback = sample_star_feedback.copy() invalid_feedback["situation"] = "太短" response = client.post('/api/star-feedbacks', json=invalid_feedback) assert response.status_code == 400 data = response.get_json() assert 'situation' in data['message'].lower() ``` ### 6.3 Rankings API 測試 ```python class TestRankingsAPI: """排名API整合測試""" def test_get_total_rankings_success(self, client, db_session): """ 測試目標: GET /api/rankings/total 測試資料: 5名員工,不同積分 預期結果: - 狀態碼: 200 - 按積分降序排列 - 包含所有必要資訊 """ # 建立測試資料 employees = [ ("員工A", 150), ("員工B", 120), ("員工C", 100), ("員工D", 80), ("員工E", 50), ] for name, points in employees: emp = EmployeePoints( employee_name=name, department="技術部", position="工程師", total_points=points, monthly_points=points ) db_session.add(emp) db_session.commit() response = client.get('/api/rankings/total') assert response.status_code == 200 data = response.get_json() rankings = data['rankings'] assert len(rankings) == 5 assert rankings[0]['employee_name'] == '員工A' assert rankings[0]['rank'] == 1 assert rankings[0]['total_points'] == 150 # 驗證排序 for i in range(len(rankings) - 1): assert rankings[i]['total_points'] >= rankings[i+1]['total_points'] def test_get_total_rankings_with_department_filter(self, client, db_session): """ 測試目標: GET /api/rankings/total?department=技術部 測試資料: - 技術部: 3名員工 - 業務部: 2名員工 預期結果: 只回傳技術部的3名員工 """ employees = [ ("員工A", "技術部", 100), ("員工B", "技術部", 80), ("員工C", "技術部", 60), ("員工D", "業務部", 90), ("員工E", "業務部", 70), ] for name, dept, points in employees: emp = EmployeePoints( employee_name=name, department=dept, position="工程師", total_points=points, monthly_points=points ) db_session.add(emp) db_session.commit() response = client.get('/api/rankings/total?department=技術部') assert response.status_code == 200 data = response.get_json() rankings = data['rankings'] assert len(rankings) == 3 for ranking in rankings: assert ranking['department'] == '技術部' def test_get_monthly_rankings_success(self, client, db_session): """ 測試目標: GET /api/rankings/monthly?year=2025&month=10 測試步驟: 1. 建立月度排名資料 2. 查詢特定月份排名 預期結果: - 狀態碼: 200 - 回傳該月份的排名資料 """ from datetime import date ranking_month = date(2025, 10, 1) rankings_data = [ ("員工A", 100, 1), ("員工B", 80, 2), ("員工C", 60, 3), ] for name, points, rank in rankings_data: ranking = MonthlyRanking( ranking_month=ranking_month, employee_name=name, department="技術部", position="工程師", total_points=points, ranking=rank ) db_session.add(ranking) db_session.commit() response = client.get('/api/rankings/monthly?year=2025&month=10') assert response.status_code == 200 data = response.get_json() assert data['year'] == 2025 assert data['month'] == 10 assert len(data['rankings']) == 3 def test_post_calculate_rankings_success(self, client, db_session): """ 測試目標: POST /api/rankings/calculate 測試步驟: 1. 建立員工積分資料 2. 呼叫排名計算API 3. 驗證排名已生成 預期結果: - 狀態碼: 200 - monthly_rankings表有新資料 - monthly_points已重置 """ # 建立測試資料 employees = [ ("員工A", 100), ("員工B", 80), ] for name, points in employees: emp = EmployeePoints( employee_name=name, department="技術部", position="工程師", total_points=points, monthly_points=points ) db_session.add(emp) db_session.commit() # 呼叫計算API response = client.post( '/api/rankings/calculate', json={"year": 2025, "month": 10} ) assert response.status_code == 200 data = response.get_json() assert data['success'] is True # 驗證排名已生成 from datetime import date rankings = db_session.query(MonthlyRanking).filter_by( ranking_month=date(2025, 10, 1) ).all() assert len(rankings) == 2 # 驗證monthly_points已重置 for emp_name, _ in employees: emp = db_session.query(EmployeePoints).filter_by( employee_name=emp_name ).first() assert emp.monthly_points == 0 ``` --- ## 7. 資料匯出測試 **測試檔案**: `tests/integration/test_export.py` ```python class TestExportFunctionality: """資料匯出功能測試""" def test_export_assessments_to_excel(self, client, db_session): """ 測試目標: GET /api/export/assessments?format=excel 測試步驟: 1. 建立測試資料 2. 請求Excel匯出 3. 驗證檔案格式與內容 預期結果: - 狀態碼: 200 - Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet - 檔案可被openpyxl讀取 """ # 建立測試資料 for i in range(5): assessment = Assessment( department="技術部", position="工程師" + str(i), employee_name="員工" + str(i), assessment_data={"L1": ["能力1"]} ) db_session.add(assessment) db_session.commit() response = client.get('/api/export/assessments?format=excel') assert response.status_code == 200 assert 'application/vnd.openxmlformats' in response.content_type # 驗證Excel檔案可讀取 import io from openpyxl import load_workbook excel_file = io.BytesIO(response.data) workbook = load_workbook(excel_file) sheet = workbook.active # 驗證有表頭 assert sheet.cell(1, 1).value is not None # 驗證有資料(5筆 + 1筆表頭) assert sheet.max_row == 6 def test_export_assessments_to_csv(self, client, db_session): """ 測試目標: GET /api/export/assessments?format=csv 預期結果: - 狀態碼: 200 - Content-Type: text/csv - UTF-8 BOM編碼(Excel相容) """ # 建立測試資料 assessment = Assessment( department="技術部", position="工程師", employee_name="測試員工", assessment_data={} ) db_session.add(assessment) db_session.commit() response = client.get('/api/export/assessments?format=csv') assert response.status_code == 200 assert 'text/csv' in response.content_type # 驗證UTF-8 BOM assert response.data.startswith(b'\xef\xbb\xbf') def test_export_star_feedbacks_with_filters(self, client, db_session): """ 測試目標: 匯出時套用篩選條件 測試資料: - 技術部: 3筆 - 業務部: 2筆 篩選條件: department=技術部 預期結果: 只匯出技術部的3筆資料 """ # 建立測試資料 for i in range(3): feedback = StarFeedback( evaluator_name="主管", evaluatee_name=f"員工{i}", evaluatee_department="技術部", evaluatee_position="工程師", situation="S", task="T", action="A", result="R", score=3, feedback_date="2025-10-15" ) db_session.add(feedback) for i in range(2): feedback = StarFeedback( evaluator_name="主管", evaluatee_name=f"員工{i+10}", evaluatee_department="業務部", evaluatee_position="專員", situation="S", task="T", action="A", result="R", score=3, feedback_date="2025-10-15" ) db_session.add(feedback) db_session.commit() response = client.get( '/api/export/star-feedbacks?format=excel&department=技術部' ) assert response.status_code == 200 # 驗證只有技術部資料 import io from openpyxl import load_workbook excel_file = io.BytesIO(response.data) workbook = load_workbook(excel_file) sheet = workbook.active # 3筆資料 + 1筆表頭 assert sheet.max_row == 4 def test_export_invalid_format(self, client): """ 測試目標: 無效的匯出格式 測試案例: format=pdf (不支援) 預期結果: - 狀態碼: 400 - 錯誤訊息 """ response = client.get('/api/export/assessments?format=pdf') assert response.status_code == 400 data = response.get_json() assert data['success'] is False assert 'format' in data['message'].lower() ``` --- ## 8. 端到端測試 (E2E) **測試檔案**: `tests/e2e/test_workflows.py` ```python class TestCompleteWorkflows: """完整業務流程端到端測試""" def test_complete_assessment_workflow(self, client, db_session, sample_capability): """ 測試目標: 完整評估流程 流程步驟: 1. 取得能力項目清單 2. 提交評估 3. 查詢評估記錄 4. 驗證資料正確 預期結果: 整個流程順利完成 """ # Step 1: 建立能力項目 db_session.add(sample_capability) db_session.commit() # Step 2: 取得能力清單 response1 = client.get('/api/capabilities') assert response1.status_code == 200 capabilities = response1.get_json()['capabilities'] assert len(capabilities) > 0 # Step 3: 提交評估 assessment_data = { "department": "技術部", "position": "工程師", "employee_name": "測試員工", "assessment_data": { "L1": [capabilities[0]['name']], "L2": [], "L3": [], "L4": [], "L5": [] } } response2 = client.post('/api/assessments', json=assessment_data) assert response2.status_code == 201 assessment_id = response2.get_json()['assessment_id'] # Step 4: 查詢評估記錄 response3 = client.get('/api/assessments') assert response3.status_code == 200 assessments = response3.get_json()['assessments'] # Step 5: 驗證資料 found = False for assessment in assessments: if assessment['id'] == assessment_id: found = True assert assessment['department'] == "技術部" assert assessment['employee_name'] == "測試員工" break assert found is True def test_complete_star_feedback_and_ranking_workflow(self, client, db_session): """ 測試目標: STAR回饋到排名的完整流程 流程步驟: 1. 提交多筆STAR回饋給不同員工 2. 驗證積分更新 3. 計算月度排名 4. 查詢排行榜 5. 驗證排名正確 預期結果: 積分與排名完整運作 """ # Step 1: 提交回饋給員工A (評分5) feedback_a = { "evaluator_name": "主管1", "evaluatee_name": "員工A", "evaluatee_department": "技術部", "evaluatee_position": "工程師", "situation": "情境描述需要足夠長度才能通過驗證測試", "task": "任務描述需要足夠長度才能通過驗證測試", "action": "行動描述需要足夠長度才能通過驗證測試", "result": "結果描述需要足夠長度才能通過驗證測試", "score": 5, "feedback_date": "2025-10-15" } response1 = client.post('/api/star-feedbacks', json=feedback_a) assert response1.status_code == 201 assert response1.get_json()['points_earned'] == 50 # Step 2: 提交回饋給員工B (評分3) feedback_b = feedback_a.copy() feedback_b['evaluatee_name'] = "員工B" feedback_b['score'] = 3 response2 = client.post('/api/star-feedbacks', json=feedback_b) assert response2.status_code == 201 assert response2.get_json()['points_earned'] == 30 # Step 3: 驗證積分 emp_a = db_session.query(EmployeePoints).filter_by( employee_name="員工A" ).first() emp_b = db_session.query(EmployeePoints).filter_by( employee_name="員工B" ).first() assert emp_a.total_points == 50 assert emp_b.total_points == 30 # Step 4: 計算排名 response3 = client.post( '/api/rankings/calculate', json={"year": 2025, "month": 10} ) assert response3.status_code == 200 # Step 5: 查詢排行榜 response4 = client.get('/api/rankings/monthly?year=2025&month=10') assert response4.status_code == 200 rankings = response4.get_json()['rankings'] assert len(rankings) == 2 # Step 6: 驗證排名順序 assert rankings[0]['employee_name'] == "員工A" assert rankings[0]['ranking'] == 1 assert rankings[0]['total_points'] == 50 assert rankings[1]['employee_name'] == "員工B" assert rankings[1]['ranking'] == 2 assert rankings[1]['total_points'] == 30 # Step 7: 驗證月度積分已重置 emp_a_updated = db_session.query(EmployeePoints).filter_by( employee_name="員工A" ).first() assert emp_a_updated.total_points == 50 # 保持 assert emp_a_updated.monthly_points == 0 # 重置 def test_export_after_data_creation(self, client, db_session): """ 測試目標: 建立資料後匯出 流程步驟: 1. 建立評估與回饋資料 2. 匯出Excel 3. 匯出CSV 4. 驗證檔案內容 預期結果: 匯出檔案包含所有建立的資料 """ # Step 1: 建立評估資料 assessment = Assessment( department="技術部", position="工程師", employee_name="匯出測試員工", assessment_data={"L1": ["能力1"]} ) db_session.add(assessment) db_session.commit() # Step 2: 匯出Excel response1 = client.get('/api/export/assessments?format=excel') assert response1.status_code == 200 import io from openpyxl import load_workbook excel_file = io.BytesIO(response1.data) workbook = load_workbook(excel_file) sheet = workbook.active # 驗證資料存在 found = False for row in sheet.iter_rows(min_row=2, values_only=True): if "匯出測試員工" in str(row): found = True break assert found is True # Step 3: 匯出CSV response2 = client.get('/api/export/assessments?format=csv') assert response2.status_code == 200 assert "匯出測試員工" in response2.data.decode('utf-8-sig') ``` --- ## 9. 效能測試 **測試檔案**: `tests/performance/test_performance.py` ```python import time import pytest class TestPerformance: """效能測試""" def test_api_response_time_under_500ms(self, client, db_session): """ 測試目標: API回應時間 < 500ms 測試API: GET /api/capabilities 預期結果: 95%請求在500ms內完成 """ # 準備資料 for i in range(10): capability = Capability( name=f"能力{i}", l1_description="描述", l2_description="描述", l3_description="描述", l4_description="描述", l5_description="描述" ) db_session.add(capability) db_session.commit() # 執行100次請求 response_times = [] for _ in range(100): start_time = time.time() response = client.get('/api/capabilities') end_time = time.time() assert response.status_code == 200 response_times.append((end_time - start_time) * 1000) # 轉為毫秒 # 計算95百分位數 response_times.sort() p95_index = int(len(response_times) * 0.95) p95_time = response_times[p95_index] print(f"\n95th percentile response time: {p95_time:.2f}ms") assert p95_time < 500, f"95th percentile ({p95_time:.2f}ms) exceeds 500ms" def test_database_query_optimization(self, db_session): """ 測試目標: 資料庫查詢效能 測試情境: 查詢1000筆資料並分頁 預期結果: 單次查詢 < 100ms """ # 建立1000筆測試資料 for i in range(1000): assessment = Assessment( department=f"部門{i % 10}", position=f"職位{i % 5}", assessment_data={} ) db_session.add(assessment) db_session.commit() # 測試分頁查詢 start_time = time.time() results = db_session.query(Assessment)\ .filter_by(department="部門1")\ .limit(20)\ .offset(0)\ .all() end_time = time.time() query_time = (end_time - start_time) * 1000 print(f"\nQuery time: {query_time:.2f}ms") assert query_time < 100, f"Query time ({query_time:.2f}ms) exceeds 100ms" def test_concurrent_requests(self, client): """ 測試目標: 並發請求處理 測試情境: 50個並發請求 預期結果: 所有請求成功完成 """ import concurrent.futures def make_request(): response = client.get('/api/capabilities') return response.status_code # 執行50個並發請求 with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor: futures = [executor.submit(make_request) for _ in range(50)] results = [f.result() for f in concurrent.futures.as_completed(futures)] # 驗證所有請求成功 assert all(status == 200 for status in results) assert len(results) == 50 ``` --- ## 10. 測試執行與CI/CD ### 10.1 本地測試執行 ```bash # 安裝測試依賴 pip install pytest pytest-cov pytest-mock # 執行所有測試 pytest # 執行特定測試檔案 pytest tests/unit/test_models.py # 執行特定測試類別 pytest tests/unit/test_models.py::TestAssessmentModel # 執行特定測試案例 pytest tests/unit/test_models.py::TestAssessmentModel::test_create_assessment_success # 顯示詳細輸出 pytest -v # 產生覆蓋率報告 pytest --cov=app --cov-report=html # 只執行失敗的測試 pytest --lf # 並行執行測試 pytest -n auto ``` ### 10.2 pytest.ini 配置 ```ini [pytest] # 測試路徑 testpaths = tests # Python檔案模式 python_files = test_*.py # 測試類別模式 python_classes = Test* # 測試函數模式 python_functions = test_* # 標記 markers = unit: Unit tests integration: Integration tests e2e: End-to-end tests slow: Slow running tests performance: Performance tests # 覆蓋率設定 addopts = --strict-markers --cov=app --cov-report=term-missing --cov-report=html --cov-fail-under=70 -ra # 忽略警告 filterwarnings = ignore::DeprecationWarning ``` ### 10.3 GitHub Actions CI/CD **.github/workflows/tests.yml**: ```yaml name: Tests on: push: branches: [ main, develop ] pull_request: branches: [ main, develop ] jobs: test: runs-on: ubuntu-latest services: mysql: image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: test_password MYSQL_DATABASE: test_db ports: - 3306:3306 options: >- --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.10' - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install pytest pytest-cov - name: Wait for MySQL run: | while ! mysqladmin ping -h"127.0.0.1" -P3306 --silent; do sleep 1 done - name: Run unit tests env: DB_HOST: 127.0.0.1 DB_PORT: 3306 DB_NAME: test_db DB_USER: root DB_PASSWORD: test_password SECRET_KEY: test_secret_key_for_ci run: | pytest tests/unit -v --cov=app --cov-report=xml - name: Run integration tests env: DB_HOST: 127.0.0.1 DB_PORT: 3306 DB_NAME: test_db DB_USER: root DB_PASSWORD: test_password SECRET_KEY: test_secret_key_for_ci run: | pytest tests/integration -v - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: file: ./coverage.xml fail_ci_if_error: true ``` ### 10.4 測試報告範例 ``` ======================== test session starts ========================= platform linux -- Python 3.10.0, pytest-7.4.0, pluggy-1.3.0 rootdir: /app plugins: cov-4.1.0 collected 156 items tests/unit/test_models.py::TestAssessmentModel::test_create_assessment_success PASSED [ 1%] tests/unit/test_models.py::TestAssessmentModel::test_assessment_requires_department PASSED [ 2%] tests/unit/test_models.py::TestAssessmentModel::test_assessment_json_serialization PASSED [ 3%] ... tests/e2e/test_workflows.py::TestCompleteWorkflows::test_complete_star_feedback_and_ranking_workflow PASSED [100%] ---------- coverage: platform linux, python 3.10.0 ----------- Name Stmts Miss Cover Missing ------------------------------------------------------------ app/__init__.py 45 2 96% 78-79 app/models.py 120 8 93% 45, 89-95 app/routes.py 230 15 93% 125, 189-195, 256 app/services.py 145 5 97% 78-82 app/validators.py 95 3 97% 56-58 app/utils.py 40 1 98% 23 ------------------------------------------------------------ TOTAL 675 34 95% ===================== 156 passed in 45.23s ====================== ``` --- ## 11. 測試資料管理 ### 11.1 測試資料Fixtures **tests/fixtures/sample_data.py**: ```python """測試資料fixtures""" def get_sample_assessments(): """範例評估資料集""" return [ { "department": "技術部", "position": "資深工程師", "employee_name": "張三", "assessment_data": { "L1": [], "L2": ["程式設計與開發"], "L3": ["系統分析與設計"], "L4": ["專案管理"], "L5": [] } }, { "department": "業務部", "position": "業務經理", "employee_name": "李四", "assessment_data": { "L1": [], "L2": [], "L3": ["客戶關係管理"], "L4": ["業務策略規劃"], "L5": [] } } ] def get_sample_star_feedbacks(): """範例STAR回饋資料集""" return [ { "evaluator_name": "王主管", "evaluatee_name": "張三", "evaluatee_department": "技術部", "evaluatee_position": "資深工程師", "situation": "專案進度落後,客戶要求提前交付核心功能", "task": "需要在一週內完成原本規劃兩週的開發任務", "action": "重新評估優先級,與團隊協調資源分配,採用敏捷開發方法快速迭代", "result": "成功提前三天交付功能,客戶滿意度提升,專案獲得額外預算", "score": 5, "feedback_date": "2025-10-01" }, { "evaluator_name": "陳主管", "evaluatee_name": "李四", "evaluatee_department": "業務部", "evaluatee_position": "業務經理", "situation": "重要客戶考慮終止合作,需要緊急挽回", "task": "在一個月內重建客戶信任並續約", "action": "深入了解客戶痛點,提出客製化解決方案,每週親自拜訪追蹤", "result": "客戶決定不僅續約還擴大合作範圍,年度合約金額增加30%", "score": 5, "feedback_date": "2025-10-05" } ] def get_sample_capabilities(): """範例能力項目資料集""" return [ { "name": "程式設計與開發", "l1_description": "能撰寫基本程式碼,完成簡單功能", "l2_description": "能獨立開發中等複雜度功能,遵循編碼規範", "l3_description": "能設計系統架構,優化程式效能", "l4_description": "能指導團隊技術方向,制定開發標準", "l5_description": "能規劃技術策略,引領技術創新" }, { "name": "專案管理", "l1_description": "能執行分配的任務,按時回報進度", "l2_description": "能獨立管理小型專案,協調資源", "l3_description": "能處理複雜專案,解決跨部門協作問題", "l4_description": "能領導多個專案,培養專案經理", "l5_description": "能制定專案管理策略,建立PMO體系" } ] ``` ### 11.2 資料庫Seed腳本 **tests/seed_test_data.py**: ```python """測試資料庫初始化腳本""" from app import create_app, db from app.models import Assessment, Capability, StarFeedback, EmployeePoints from tests.fixtures.sample_data import ( get_sample_assessments, get_sample_star_feedbacks, get_sample_capabilities ) def seed_test_database(): """初始化測試資料庫""" app = create_app('testing') with app.app_context(): # 清空資料庫 db.drop_all() db.create_all() # 建立能力項目 for cap_data in get_sample_capabilities(): capability = Capability(**cap_data) db.session.add(capability) # 建立評估資料 for assess_data in get_sample_assessments(): assessment = Assessment(**assess_data) db.session.add(assessment) # 建立STAR回饋 for feedback_data in get_sample_star_feedbacks(): feedback = StarFeedback(**feedback_data) db.session.add(feedback) # 更新員工積分 emp_name = feedback_data['evaluatee_name'] points = feedback_data['score'] * 10 employee = db.session.query(EmployeePoints).filter_by( employee_name=emp_name ).first() if not employee: employee = EmployeePoints( employee_name=emp_name, department=feedback_data['evaluatee_department'], position=feedback_data['evaluatee_position'], total_points=points, monthly_points=points ) db.session.add(employee) else: employee.total_points += points employee.monthly_points += points db.session.commit() print("✅ 測試資料初始化完成") if __name__ == '__main__': seed_test_database() ``` --- ## 12. 測試最佳實踐 ### 12.1 TDD開發流程 ``` 1. 寫一個失敗的測試 (RED) ├── 明確定義預期行為 ├── 寫出測試案例 └── 執行測試,確認失敗 2. 寫最小程式碼讓測試通過 (GREEN) ├── 實作功能邏輯 ├── 執行測試 └── 確認測試通過 3. 重構程式碼 (REFACTOR) ├── 優化程式結構 ├── 移除重複代碼 ├── 改善可讀性 └── 確保測試仍然通過 4. 重複循環 └── 繼續下一個功能 ``` ### 12.2 測試命名規範 ```python # ✅ 好的測試名稱 def test_create_assessment_with_valid_data_should_succeed(): """測試名稱清楚描述: 動作 + 條件 + 預期結果""" pass def test_calculate_points_with_score_5_should_return_50(): """明確的輸入與輸出""" pass # ❌ 不好的測試名稱 def test_assessment(): """太籠統""" pass def test_1(): """無意義""" pass ``` ### 12.3 測試獨立性原則 ```python # ✅ 每個測試獨立運作 class TestIndependent: def test_case_1(self, db_session): # 測試1有自己的資料 data = create_test_data() # ... 執行測試 def test_case_2(self, db_session): # 測試2不依賴測試1 data = create_different_test_data() # ... 執行測試 # ❌ 測試之間有依賴性 class TestDependent: shared_data = None def test_case_1(self): self.shared_data = create_data() def test_case_2(self): # 依賴test_case_1的結果 ❌ use_data(self.shared_data) ``` ### 12.4 AAA模式 ```python def test_with_aaa_pattern(): """ AAA模式: Arrange-Act-Assert """ # Arrange (準備): 設定測試資料與環境 employee = EmployeePoints( employee_name="測試員工", department="技術部", position="工程師" ) initial_points = 30 additional_points = 20 # Act (執行): 執行要測試的操作 employee.total_points = initial_points employee.total_points += additional_points # Assert (驗證): 驗證結果 assert employee.total_points == 50 ``` ### 12.5 測試覆蓋率目標 ``` 優先級排序: 1. 🔴 核心業務邏輯: 100% (積分計算、排名邏輯) 2. 🟡 API端點: 100% (所有路由) 3. 🟡 資料模型: 90%+ (CRUD操作) 4. 🟢 工具函數: 80%+ (輔助功能) 5. 🟢 UI邏輯: 70%+ (前端交互) 不追求100%覆蓋率: - 簡單的getter/setter - 框架自動生成的程式碼 - 第三方套件整合 ``` --- ## 13. 持續改進 ### 13.1 測試債務追蹤 | 項目 | 當前狀態 | 目標 | 優先級 | |-----|---------|------|--------| | 單元測試覆蓋率 | 75% | 80%+ | 🔴 高 | | 整合測試 | 60% | 70%+ | 🟡 中 | | E2E測試 | 核心流程 | 完整流程 | 🟡 中 | | 效能測試 | 基礎 | 完整benchmark | 🟢 低 | | 安全測試 | 未實作 | SQL注入、XSS測試 | 🔴 高 | ### 13.2 測試審查檢查清單 ``` □ 測試名稱清晰描述意圖 □ 使用AAA模式組織測試 □ 測試獨立且可重複執行 □ 包含正常與異常情境 □ 驗證邊界條件 □ Mock外部依賴 □ 測試執行速度合理(<10s) □ 有適當的測試文件 ``` ### 13.3 定期測試維護 ```python # 每月審查 - 移除過時的測試 - 更新測試資料 - 檢查測試執行時間 - 修復不穩定的測試 # 每季審查 - 評估測試覆蓋率 - 重構重複的測試程式碼 - 更新測試策略 - 培訓團隊TDD實踐 ``` --- ## 14. 附錄 ### 14.1 常用測試指令 ```bash # 快速測試 pytest -x # 遇到第一個失敗就停止 pytest --lf # 只執行上次失敗的測試 pytest -k "assessment" # 執行名稱包含"assessment"的測試 pytest -m unit # 執行標記為unit的測試 # 覆蓋率相關 pytest --cov=app # 顯示覆蓋率 pytest --cov-report=html # HTML格式報告 pytest --cov-report=term-missing # 顯示缺失的行號 # 效能分析 pytest --durations=10 # 顯示最慢的10個測試 pytest --profile # 執行效能分析 # 並行執行 pytest -n 4 # 使用4個worker並行 pytest -n auto # 自動決定worker數量 # Debug模式 pytest --pdb # 失敗時進入debugger pytest -s # 顯示print輸出 pytest -vv # 非常詳細的輸出 ``` ### 14.2 Mock範例 ```python from unittest.mock import Mock, patch def test_with_mock(db_session): """使用Mock測試外部依賴""" # Mock資料庫查詢 with patch('app.services.db_session.query') as mock_query: mock_query.return_value.filter_by.return_value.first.return_value = Mock( employee_name="測試員工", total_points=100 ) # 執行測試 result = get_employee_points("測試員工") # 驗證 assert result == 100 mock_query.assert_called_once() ``` ### 14.3 參考資源 - [pytest官方文件](https://docs.pytest.org/) - [Test-Driven Development書籍](https://www.oreilly.com/library/view/test-driven-development/0321146530/) - [Python Testing with pytest](https://pragprog.com/titles/bopytest/python-testing-with-pytest/) - [Flask Testing文件](https://flask.palletsprojects.com/en/latest/testing/) --- **文件結束** **版本**: 1.0 **最後更新**: 2025年10月15日 **下次審查**: 2025年11月15日