Files
1015_IT_behavior_alignment_V2/# 夥伴對齊系統 - 測試驅動開發文件 (TDD).txt
2025-10-28 15:50:53 +08:00

2491 lines
73 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 夥伴對齊系統 - 測試驅動開發文件 (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日