Major Features: - ✅ Multi-tenant architecture (tenant isolation) - ✅ Employee CRUD with lifecycle management (onboarding/offboarding) - ✅ Department tree structure with email domain management - ✅ Company info management (single-record editing) - ✅ System functions CRUD (permission management) - ✅ Email account management (multi-account per employee) - ✅ Keycloak SSO integration (auth.lab.taipei) - ✅ Redis session storage (10.1.0.254:6379) - Solves Cookie 4KB limitation - Cross-system session sharing - Sliding expiration (8 hours) - Automatic token refresh Technical Stack: Backend: - FastAPI + SQLAlchemy - PostgreSQL 16 (10.1.0.20:5433) - Keycloak Admin API integration - Docker Mailserver integration (SSH) - Alembic migrations Frontend: - Next.js 14 (App Router) - NextAuth 4 with Keycloak Provider - Redis session storage (ioredis) - Tailwind CSS Infrastructure: - Redis 7 (10.1.0.254:6379) - Session + Cache - Keycloak 26.1.0 (auth.lab.taipei) - Docker Mailserver (10.1.0.254) Architecture Highlights: - Session管理由 Keycloak + Redis 統一控制 - 支援多系統 (HR/WebMail/Calendar/Drive/Office) 共享 session - Token 自動刷新,異質服務整合 - 未來可無縫遷移到雲端 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
305 lines
6.4 KiB
Markdown
305 lines
6.4 KiB
Markdown
# TDD 開發指南 - HR Portal 前端
|
||
|
||
**建立日期**: 2026-02-21
|
||
**測試框架**: Vitest + React Testing Library
|
||
|
||
---
|
||
|
||
## 📋 目錄結構
|
||
|
||
```
|
||
frontend/
|
||
├── services/
|
||
│ ├── __tests__/
|
||
│ │ └── onboarding.service.test.ts # API Service 測試
|
||
│ └── onboarding.service.ts # API Service 實作
|
||
├── components/
|
||
│ ├── __tests__/
|
||
│ │ └── OnboardingForm.test.tsx # 組件測試 (待建立)
|
||
│ └── OnboardingForm.tsx # 組件實作 (待建立)
|
||
├── types/
|
||
│ └── onboarding.ts # TypeScript 型別定義
|
||
├── vitest.config.ts # Vitest 配置
|
||
└── vitest.setup.ts # 測試環境設置
|
||
```
|
||
|
||
---
|
||
|
||
## 🚀 快速開始
|
||
|
||
### 1. 安裝依賴
|
||
|
||
```bash
|
||
cd q:/porscheworld_develop/hr-portal/frontend
|
||
npm install
|
||
```
|
||
|
||
**需要安裝的測試相關套件**:
|
||
```json
|
||
{
|
||
"devDependencies": {
|
||
"@testing-library/jest-dom": "^6.1.5",
|
||
"@testing-library/react": "^14.1.2",
|
||
"@testing-library/user-event": "^14.5.1",
|
||
"@vitejs/plugin-react": "^4.2.1",
|
||
"@vitest/ui": "^1.0.4",
|
||
"jsdom": "^23.0.1",
|
||
"vitest": "^1.0.4"
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 執行測試
|
||
|
||
```bash
|
||
# 執行所有測試
|
||
npm test
|
||
|
||
# 執行測試並開啟 UI 介面
|
||
npm run test:ui
|
||
|
||
# 執行測試並生成覆蓋率報告
|
||
npm run test:coverage
|
||
|
||
# 監視模式(自動重跑)
|
||
npm test -- --watch
|
||
```
|
||
|
||
---
|
||
|
||
## 🎯 TDD 工作流程
|
||
|
||
### Red-Green-Refactor 循環
|
||
|
||
```
|
||
1. RED → 先寫失敗的測試
|
||
2. GREEN → 寫最少的程式碼讓測試通過
|
||
3. REFACTOR → 重構程式碼,保持測試通過
|
||
```
|
||
|
||
### 範例:API Service TDD
|
||
|
||
#### Step 1: RED - 寫失敗的測試
|
||
|
||
```typescript
|
||
// services/__tests__/onboarding.service.test.ts
|
||
import { describe, it, expect } from 'vitest'
|
||
import { onboardingService } from '../onboarding.service'
|
||
|
||
describe('OnboardingService', () => {
|
||
it('should successfully onboard an employee', async () => {
|
||
const request = {
|
||
resume_id: 1,
|
||
keycloak_user_id: '550e8400-...',
|
||
keycloak_username: 'wang.ming',
|
||
hire_date: '2026-02-21',
|
||
departments: [{ department_id: 9, position: '工程師' }],
|
||
role_ids: [1],
|
||
}
|
||
|
||
const result = await onboardingService.onboardEmployee(request)
|
||
|
||
expect(result).toHaveProperty('message')
|
||
expect(result).toHaveProperty('employee')
|
||
})
|
||
})
|
||
```
|
||
|
||
執行測試:
|
||
```bash
|
||
npm test
|
||
# ❌ 測試失敗:onboardingService 不存在
|
||
```
|
||
|
||
#### Step 2: GREEN - 實作最少程式碼
|
||
|
||
```typescript
|
||
// services/onboarding.service.ts
|
||
import axios from 'axios'
|
||
|
||
class OnboardingService {
|
||
async onboardEmployee(request: OnboardingRequest) {
|
||
const response = await axios.post('/api/v1/emp-lifecycle/onboard', request)
|
||
return response.data
|
||
}
|
||
}
|
||
|
||
export const onboardingService = new OnboardingService()
|
||
```
|
||
|
||
執行測試:
|
||
```bash
|
||
npm test
|
||
# ✅ 測試通過
|
||
```
|
||
|
||
#### Step 3: REFACTOR - 重構
|
||
|
||
```typescript
|
||
// 新增型別定義、錯誤處理、環境變數等
|
||
```
|
||
|
||
---
|
||
|
||
## 📦 已完成的測試
|
||
|
||
### ✅ API Service 層
|
||
|
||
**檔案**: `services/__tests__/onboarding.service.test.ts`
|
||
|
||
**測試案例**:
|
||
1. ✅ `onboardEmployee` - 成功到職
|
||
2. ✅ `onboardEmployee` - API 失敗處理
|
||
3. ✅ `getEmployeeStatus` - 查詢員工狀態
|
||
4. ✅ `getEmployeeStatus` - 員工不存在
|
||
5. ✅ `offboardEmployee` - 成功離職
|
||
|
||
**執行測試**:
|
||
```bash
|
||
npm test services/__tests__/onboarding.service.test.ts
|
||
```
|
||
|
||
---
|
||
|
||
## 🔧 下一步:組件測試
|
||
|
||
### 待建立的測試
|
||
|
||
#### 1. OnboardingForm 組件測試
|
||
|
||
```typescript
|
||
// components/__tests__/OnboardingForm.test.tsx
|
||
import { describe, it, expect } from 'vitest'
|
||
import { render, screen, fireEvent } from '@testing-library/react'
|
||
import { OnboardingForm } from '../OnboardingForm'
|
||
|
||
describe('OnboardingForm', () => {
|
||
it('should render all required fields', () => {
|
||
render(<OnboardingForm />)
|
||
|
||
expect(screen.getByLabelText('員工姓名')).toBeInTheDocument()
|
||
expect(screen.getByLabelText('Keycloak 使用者名稱')).toBeInTheDocument()
|
||
expect(screen.getByLabelText('到職日期')).toBeInTheDocument()
|
||
})
|
||
|
||
it('should validate required fields', async () => {
|
||
render(<OnboardingForm />)
|
||
|
||
const submitButton = screen.getByRole('button', { name: /送出/i })
|
||
fireEvent.click(submitButton)
|
||
|
||
expect(await screen.findByText('此欄位為必填')).toBeInTheDocument()
|
||
})
|
||
|
||
it('should submit form successfully', async () => {
|
||
const onSubmit = vi.fn()
|
||
render(<OnboardingForm onSubmit={onSubmit} />)
|
||
|
||
// Fill form...
|
||
fireEvent.click(screen.getByRole('button', { name: /送出/i }))
|
||
|
||
expect(onSubmit).toHaveBeenCalled()
|
||
})
|
||
})
|
||
```
|
||
|
||
#### 2. EmployeeStatusCard 組件測試
|
||
|
||
```typescript
|
||
// 顯示員工詳細資訊的卡片組件
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 測試覆蓋率目標
|
||
|
||
| 類型 | 目標 | 當前 |
|
||
|------|------|------|
|
||
| API Service | 100% | 100% ✅ |
|
||
| Components | 80% | 0% ⏳ |
|
||
| Hooks | 80% | 0% ⏳ |
|
||
| Utilities | 90% | 0% ⏳ |
|
||
|
||
---
|
||
|
||
## 🎨 測試最佳實踐
|
||
|
||
### 1. 測試命名
|
||
|
||
```typescript
|
||
// ✅ Good: 描述行為,不是實作
|
||
it('should display error when API fails', () => {})
|
||
|
||
// ❌ Bad: 測試實作細節
|
||
it('should call axios.post', () => {})
|
||
```
|
||
|
||
### 2. AAA 模式
|
||
|
||
```typescript
|
||
it('should do something', () => {
|
||
// Arrange (準備)
|
||
const data = { ... }
|
||
|
||
// Act (執行)
|
||
const result = doSomething(data)
|
||
|
||
// Assert (斷言)
|
||
expect(result).toBe(expected)
|
||
})
|
||
```
|
||
|
||
### 3. 隔離測試
|
||
|
||
```typescript
|
||
// ✅ Good: 每個測試獨立
|
||
describe('MyComponent', () => {
|
||
beforeEach(() => {
|
||
// 每個測試前重置
|
||
cleanup()
|
||
})
|
||
})
|
||
|
||
// ❌ Bad: 測試間有依賴
|
||
```
|
||
|
||
### 4. Mock 外部依賴
|
||
|
||
```typescript
|
||
// Mock axios
|
||
vi.mock('axios')
|
||
|
||
// Mock Next.js router
|
||
vi.mock('next/navigation')
|
||
```
|
||
|
||
---
|
||
|
||
## 🐛 常見問題
|
||
|
||
### Q1: `ReferenceError: describe is not defined`
|
||
|
||
**解決**: 確認 `vitest.config.ts` 中有設定 `globals: true`
|
||
|
||
### Q2: `TypeError: Cannot read property 'xxx' of undefined`
|
||
|
||
**解決**: 檢查是否正確 mock 了依賴
|
||
|
||
### Q3: 測試執行很慢
|
||
|
||
**解決**:
|
||
- 使用 `vi.mock()` 避免真實 API 呼叫
|
||
- 使用 `--run` 參數執行一次性測試
|
||
|
||
---
|
||
|
||
## 📚 參考資源
|
||
|
||
- [Vitest 官方文件](https://vitest.dev/)
|
||
- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/)
|
||
- [Jest DOM Matchers](https://github.com/testing-library/jest-dom)
|
||
|
||
---
|
||
|
||
**下一步**: 執行 `npm install` 安裝測試依賴,然後執行 `npm test` 確認測試環境正常運作。
|