- Replaced SELECT* queries in 8 models with explicit columns. - Began modularizing work-report-calendar.js by creating CalendarAPI.js, CalendarState.js, and CalendarView.js. - Refactored manage-project.js to use global API helpers. - Fixed API container crash by adding missing volume mounts to docker-compose.yml. - Added new migration for missing columns in the projects table. - Documented current DB schema and deployment notes.
918 lines
24 KiB
Markdown
918 lines
24 KiB
Markdown
# TK-FB-Project 테스트 가이드
|
|
|
|
## 목차
|
|
1. [개요](#개요)
|
|
2. [테스트 환경 설정](#테스트-환경-설정)
|
|
3. [테스트 작성 가이드](#테스트-작성-가이드)
|
|
4. [실전 예제](#실전-예제)
|
|
5. [테스트 실행](#테스트-실행)
|
|
6. [모범 사례](#모범-사례)
|
|
|
|
---
|
|
|
|
## 개요
|
|
|
|
이 프로젝트는 **Jest**를 사용하여 테스트를 작성합니다.
|
|
|
|
### 테스트 계층
|
|
```
|
|
┌─────────────────────────────────┐
|
|
│ API 통합 테스트 (E2E) │ ← supertest로 실제 HTTP 요청
|
|
├─────────────────────────────────┤
|
|
│ 컨트롤러 테스트 │ ← req/res 모킹
|
|
├─────────────────────────────────┤
|
|
│ 서비스 레이어 테스트 │ ← 비즈니스 로직 (DB 모킹)
|
|
├─────────────────────────────────┤
|
|
│ 모델 테스트 │ ← DB 쿼리 로직
|
|
└─────────────────────────────────┘
|
|
```
|
|
|
|
### 우선순위
|
|
1. **서비스 레이어 테스트** ⭐ (비즈니스 로직, 가장 중요)
|
|
2. **컨트롤러 테스트** (API 엔드포인트)
|
|
3. **통합 테스트** (실제 DB 사용)
|
|
|
|
---
|
|
|
|
## 테스트 환경 설정
|
|
|
|
### 1단계: 필요한 패키지 설치
|
|
|
|
```bash
|
|
cd /Users/hyungiahn/Documents/code/TK-FB-Project/api.hyungi.net
|
|
|
|
npm install --save-dev jest supertest @types/jest
|
|
npm install --save-dev jest-mock-extended
|
|
```
|
|
|
|
### 2단계: Jest 설정 파일 생성
|
|
|
|
**`jest.config.js`** 파일을 프로젝트 루트에 생성:
|
|
|
|
```javascript
|
|
module.exports = {
|
|
testEnvironment: 'node',
|
|
coverageDirectory: 'coverage',
|
|
collectCoverageFrom: [
|
|
'controllers/**/*.js',
|
|
'services/**/*.js',
|
|
'models/**/*.js',
|
|
'!**/node_modules/**',
|
|
'!**/coverage/**',
|
|
'!**/tests/**'
|
|
],
|
|
testMatch: [
|
|
'**/tests/**/*.test.js',
|
|
'**/tests/**/*.spec.js'
|
|
],
|
|
setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
|
|
verbose: true,
|
|
collectCoverage: true,
|
|
coverageReporters: ['text', 'lcov', 'html'],
|
|
testTimeout: 10000
|
|
};
|
|
```
|
|
|
|
### 3단계: 테스트 디렉토리 구조
|
|
|
|
```
|
|
api.hyungi.net/
|
|
├── tests/
|
|
│ ├── setup.js # 전역 테스트 설정
|
|
│ ├── helpers/
|
|
│ │ ├── dbHelper.js # DB 모킹 헬퍼
|
|
│ │ └── mockData.js # 테스트용 더미 데이터
|
|
│ ├── unit/
|
|
│ │ ├── services/ # 서비스 단위 테스트
|
|
│ │ │ ├── workReportService.test.js
|
|
│ │ │ ├── attendanceService.test.js
|
|
│ │ │ └── ...
|
|
│ │ ├── controllers/ # 컨트롤러 테스트
|
|
│ │ │ ├── workReportController.test.js
|
|
│ │ │ └── ...
|
|
│ │ └── models/ # 모델 테스트
|
|
│ │ └── ...
|
|
│ └── integration/ # 통합 테스트
|
|
│ ├── workReport.test.js
|
|
│ └── ...
|
|
└── package.json
|
|
```
|
|
|
|
### 4단계: package.json에 스크립트 추가
|
|
|
|
```json
|
|
{
|
|
"scripts": {
|
|
"test": "jest",
|
|
"test:watch": "jest --watch",
|
|
"test:coverage": "jest --coverage",
|
|
"test:unit": "jest tests/unit",
|
|
"test:integration": "jest tests/integration",
|
|
"test:verbose": "jest --verbose"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 테스트 작성 가이드
|
|
|
|
### 서비스 레이어 테스트 패턴
|
|
|
|
서비스 레이어는 **DB를 모킹**하여 테스트합니다.
|
|
|
|
#### 기본 구조
|
|
|
|
```javascript
|
|
const serviceName = require('../../services/serviceName');
|
|
const { ValidationError, NotFoundError, DatabaseError } = require('../../utils/errors');
|
|
|
|
// DB 모킹
|
|
jest.mock('../../models/modelName');
|
|
const ModelName = require('../../models/modelName');
|
|
|
|
describe('ServiceName', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('functionName', () => {
|
|
it('should ... (성공 케이스)', async () => {
|
|
// Arrange (준비)
|
|
const mockData = { /* ... */ };
|
|
ModelName.methodName.mockResolvedValue(mockData);
|
|
|
|
// Act (실행)
|
|
const result = await serviceName.functionName(params);
|
|
|
|
// Assert (검증)
|
|
expect(result).toEqual(expectedResult);
|
|
expect(ModelName.methodName).toHaveBeenCalledWith(expectedParams);
|
|
});
|
|
|
|
it('should throw ValidationError when ... (실패 케이스)', async () => {
|
|
// Arrange
|
|
const invalidParams = null;
|
|
|
|
// Act & Assert
|
|
await expect(serviceName.functionName(invalidParams))
|
|
.rejects.toThrow(ValidationError);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### 컨트롤러 테스트 패턴
|
|
|
|
컨트롤러는 **서비스 레이어를 모킹**하여 테스트합니다.
|
|
|
|
```javascript
|
|
const controllerName = require('../../controllers/controllerName');
|
|
|
|
// 서비스 모킹
|
|
jest.mock('../../services/serviceName');
|
|
const serviceName = require('../../services/serviceName');
|
|
|
|
describe('ControllerName', () => {
|
|
let req, res;
|
|
|
|
beforeEach(() => {
|
|
// req, res 모킹 객체 초기화
|
|
req = {
|
|
body: {},
|
|
params: {},
|
|
query: {},
|
|
user: { id: 1, role: 'admin' }
|
|
};
|
|
res = {
|
|
json: jest.fn().mockReturnThis(),
|
|
status: jest.fn().mockReturnThis()
|
|
};
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('GET /endpoint', () => {
|
|
it('should return data successfully', async () => {
|
|
// Arrange
|
|
const mockData = { /* ... */ };
|
|
serviceName.getData.mockResolvedValue(mockData);
|
|
|
|
// Act
|
|
await controllerName.getData(req, res);
|
|
|
|
// Assert
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
success: true,
|
|
data: mockData,
|
|
message: expect.any(String)
|
|
});
|
|
expect(serviceName.getData).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### 통합 테스트 패턴 (E2E)
|
|
|
|
실제 HTTP 요청으로 테스트합니다.
|
|
|
|
```javascript
|
|
const request = require('supertest');
|
|
const app = require('../../index');
|
|
const { getDb } = require('../../dbPool');
|
|
|
|
describe('WorkReport API Integration Tests', () => {
|
|
let db;
|
|
|
|
beforeAll(async () => {
|
|
db = await getDb();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await db.end();
|
|
});
|
|
|
|
describe('POST /api/work-reports', () => {
|
|
it('should create work report', async () => {
|
|
const response = await request(app)
|
|
.post('/api/work-reports')
|
|
.set('Authorization', 'Bearer ' + testToken)
|
|
.send({
|
|
report_date: '2025-12-11',
|
|
worker_id: 1,
|
|
// ... 기타 필드
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data).toHaveProperty('workReport_ids');
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 실전 예제
|
|
|
|
### 예제 1: workReportService 테스트
|
|
|
|
**`tests/unit/services/workReportService.test.js`**
|
|
|
|
```javascript
|
|
const workReportService = require('../../../services/workReportService');
|
|
const { ValidationError, NotFoundError, DatabaseError } = require('../../../utils/errors');
|
|
|
|
// 모델 모킹
|
|
jest.mock('../../../models/workReportModel');
|
|
const workReportModel = require('../../../models/workReportModel');
|
|
|
|
describe('WorkReportService', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('createWorkReportService', () => {
|
|
it('단일 보고서를 성공적으로 생성해야 함', async () => {
|
|
// Arrange
|
|
const reportData = {
|
|
report_date: '2025-12-11',
|
|
worker_id: 1,
|
|
project_id: 1,
|
|
work_hours: 8
|
|
};
|
|
|
|
// workReportModel.create가 콜백 형태이므로 모킹 설정
|
|
workReportModel.create = jest.fn((data, callback) => {
|
|
callback(null, 123); // insertId = 123
|
|
});
|
|
|
|
// Act
|
|
const result = await workReportService.createWorkReportService(reportData);
|
|
|
|
// Assert
|
|
expect(result).toEqual({ workReport_ids: [123] });
|
|
expect(workReportModel.create).toHaveBeenCalledTimes(1);
|
|
expect(workReportModel.create).toHaveBeenCalledWith(
|
|
reportData,
|
|
expect.any(Function)
|
|
);
|
|
});
|
|
|
|
it('다중 보고서를 성공적으로 생성해야 함', async () => {
|
|
// Arrange
|
|
const reportsData = [
|
|
{ report_date: '2025-12-11', worker_id: 1, work_hours: 8 },
|
|
{ report_date: '2025-12-11', worker_id: 2, work_hours: 7 }
|
|
];
|
|
|
|
let callCount = 0;
|
|
workReportModel.create = jest.fn((data, callback) => {
|
|
callCount++;
|
|
callback(null, 100 + callCount);
|
|
});
|
|
|
|
// Act
|
|
const result = await workReportService.createWorkReportService(reportsData);
|
|
|
|
// Assert
|
|
expect(result).toEqual({ workReport_ids: [101, 102] });
|
|
expect(workReportModel.create).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('빈 배열이면 ValidationError를 던져야 함', async () => {
|
|
// Act & Assert
|
|
await expect(workReportService.createWorkReportService([]))
|
|
.rejects.toThrow(ValidationError);
|
|
|
|
await expect(workReportService.createWorkReportService([]))
|
|
.rejects.toThrow('보고서 데이터가 필요합니다');
|
|
});
|
|
|
|
it('DB 오류 시 DatabaseError를 던져야 함', async () => {
|
|
// Arrange
|
|
const reportData = { report_date: '2025-12-11', worker_id: 1 };
|
|
|
|
workReportModel.create = jest.fn((data, callback) => {
|
|
callback(new Error('DB connection failed'), null);
|
|
});
|
|
|
|
// Act & Assert
|
|
await expect(workReportService.createWorkReportService(reportData))
|
|
.rejects.toThrow(DatabaseError);
|
|
});
|
|
});
|
|
|
|
describe('getWorkReportByIdService', () => {
|
|
it('ID로 보고서를 조회해야 함', async () => {
|
|
// Arrange
|
|
const mockReport = {
|
|
id: 123,
|
|
report_date: '2025-12-11',
|
|
worker_id: 1,
|
|
work_hours: 8
|
|
};
|
|
|
|
workReportModel.getById = jest.fn((id, callback) => {
|
|
callback(null, mockReport);
|
|
});
|
|
|
|
// Act
|
|
const result = await workReportService.getWorkReportByIdService(123);
|
|
|
|
// Assert
|
|
expect(result).toEqual(mockReport);
|
|
expect(workReportModel.getById).toHaveBeenCalledWith(123, expect.any(Function));
|
|
});
|
|
|
|
it('보고서가 없으면 NotFoundError를 던져야 함', async () => {
|
|
// Arrange
|
|
workReportModel.getById = jest.fn((id, callback) => {
|
|
callback(null, null);
|
|
});
|
|
|
|
// Act & Assert
|
|
await expect(workReportService.getWorkReportByIdService(999))
|
|
.rejects.toThrow(NotFoundError);
|
|
|
|
await expect(workReportService.getWorkReportByIdService(999))
|
|
.rejects.toThrow('작업 보고서를 찾을 수 없습니다');
|
|
});
|
|
|
|
it('ID가 없으면 ValidationError를 던져야 함', async () => {
|
|
// Act & Assert
|
|
await expect(workReportService.getWorkReportByIdService(null))
|
|
.rejects.toThrow(ValidationError);
|
|
});
|
|
});
|
|
|
|
describe('updateWorkReportService', () => {
|
|
it('보고서를 성공적으로 수정해야 함', async () => {
|
|
// Arrange
|
|
const updateData = { work_hours: 9 };
|
|
|
|
workReportModel.update = jest.fn((id, data, callback) => {
|
|
callback(null, 1); // affectedRows = 1
|
|
});
|
|
|
|
// Act
|
|
const result = await workReportService.updateWorkReportService(123, updateData);
|
|
|
|
// Assert
|
|
expect(result).toEqual({ changes: 1 });
|
|
expect(workReportModel.update).toHaveBeenCalledWith(
|
|
123,
|
|
updateData,
|
|
expect.any(Function)
|
|
);
|
|
});
|
|
|
|
it('수정할 보고서가 없으면 NotFoundError를 던져야 함', async () => {
|
|
// Arrange
|
|
workReportModel.update = jest.fn((id, data, callback) => {
|
|
callback(null, 0); // affectedRows = 0
|
|
});
|
|
|
|
// Act & Assert
|
|
await expect(workReportService.updateWorkReportService(999, {}))
|
|
.rejects.toThrow(NotFoundError);
|
|
});
|
|
});
|
|
|
|
describe('removeWorkReportService', () => {
|
|
it('보고서를 성공적으로 삭제해야 함', async () => {
|
|
// Arrange
|
|
workReportModel.remove = jest.fn((id, callback) => {
|
|
callback(null, 1);
|
|
});
|
|
|
|
// Act
|
|
const result = await workReportService.removeWorkReportService(123);
|
|
|
|
// Assert
|
|
expect(result).toEqual({ changes: 1 });
|
|
expect(workReportModel.remove).toHaveBeenCalledWith(123, expect.any(Function));
|
|
});
|
|
|
|
it('삭제할 보고서가 없으면 NotFoundError를 던져야 함', async () => {
|
|
// Arrange
|
|
workReportModel.remove = jest.fn((id, callback) => {
|
|
callback(null, 0);
|
|
});
|
|
|
|
// Act & Assert
|
|
await expect(workReportService.removeWorkReportService(999))
|
|
.rejects.toThrow(NotFoundError);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### 예제 2: workReportController 테스트
|
|
|
|
**`tests/unit/controllers/workReportController.test.js`**
|
|
|
|
```javascript
|
|
const workReportController = require('../../../controllers/workReportController');
|
|
|
|
// 서비스 모킹
|
|
jest.mock('../../../services/workReportService');
|
|
const workReportService = require('../../../services/workReportService');
|
|
|
|
describe('WorkReportController', () => {
|
|
let req, res;
|
|
|
|
beforeEach(() => {
|
|
req = {
|
|
body: {},
|
|
params: {},
|
|
query: {},
|
|
user: { id: 1, username: 'test_user', role: 'admin' }
|
|
};
|
|
res = {
|
|
json: jest.fn().mockReturnThis(),
|
|
status: jest.fn().mockReturnThis()
|
|
};
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('createWorkReport', () => {
|
|
it('단일 작업 보고서를 생성해야 함', async () => {
|
|
// Arrange
|
|
req.body = {
|
|
report_date: '2025-12-11',
|
|
worker_id: 1,
|
|
work_hours: 8
|
|
};
|
|
|
|
const mockResult = { workReport_ids: [123] };
|
|
workReportService.createWorkReportService.mockResolvedValue(mockResult);
|
|
|
|
// Act
|
|
await workReportController.createWorkReport(req, res);
|
|
|
|
// Assert
|
|
expect(workReportService.createWorkReportService).toHaveBeenCalledWith(req.body);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
success: true,
|
|
data: mockResult,
|
|
message: '작업 보고서가 성공적으로 생성되었습니다'
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getWorkReportsByDate', () => {
|
|
it('날짜별 작업 보고서를 조회해야 함', async () => {
|
|
// Arrange
|
|
req.params = { date: '2025-12-11' };
|
|
const mockReports = [
|
|
{ id: 1, report_date: '2025-12-11', work_hours: 8 },
|
|
{ id: 2, report_date: '2025-12-11', work_hours: 7 }
|
|
];
|
|
|
|
workReportService.getWorkReportsByDateService.mockResolvedValue(mockReports);
|
|
|
|
// Act
|
|
await workReportController.getWorkReportsByDate(req, res);
|
|
|
|
// Assert
|
|
expect(workReportService.getWorkReportsByDateService).toHaveBeenCalledWith('2025-12-11');
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
success: true,
|
|
data: mockReports,
|
|
message: '작업 보고서 조회 성공'
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getWorkReportById', () => {
|
|
it('ID로 작업 보고서를 조회해야 함', async () => {
|
|
// Arrange
|
|
req.params = { id: '123' };
|
|
const mockReport = { id: 123, work_hours: 8 };
|
|
|
|
workReportService.getWorkReportByIdService.mockResolvedValue(mockReport);
|
|
|
|
// Act
|
|
await workReportController.getWorkReportById(req, res);
|
|
|
|
// Assert
|
|
expect(workReportService.getWorkReportByIdService).toHaveBeenCalledWith('123');
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
success: true,
|
|
data: mockReport,
|
|
message: '작업 보고서 조회 성공'
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('updateWorkReport', () => {
|
|
it('작업 보고서를 수정해야 함', async () => {
|
|
// Arrange
|
|
req.params = { id: '123' };
|
|
req.body = { work_hours: 9 };
|
|
const mockResult = { changes: 1 };
|
|
|
|
workReportService.updateWorkReportService.mockResolvedValue(mockResult);
|
|
|
|
// Act
|
|
await workReportController.updateWorkReport(req, res);
|
|
|
|
// Assert
|
|
expect(workReportService.updateWorkReportService).toHaveBeenCalledWith('123', req.body);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
success: true,
|
|
data: mockResult,
|
|
message: '작업 보고서가 성공적으로 수정되었습니다'
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('removeWorkReport', () => {
|
|
it('작업 보고서를 삭제해야 함', async () => {
|
|
// Arrange
|
|
req.params = { id: '123' };
|
|
const mockResult = { changes: 1 };
|
|
|
|
workReportService.removeWorkReportService.mockResolvedValue(mockResult);
|
|
|
|
// Act
|
|
await workReportController.removeWorkReport(req, res);
|
|
|
|
// Assert
|
|
expect(workReportService.removeWorkReportService).toHaveBeenCalledWith('123');
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
success: true,
|
|
data: mockResult,
|
|
message: '작업 보고서가 성공적으로 삭제되었습니다'
|
|
});
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
### 예제 3: 통합 테스트 (E2E)
|
|
|
|
**`tests/integration/workReport.test.js`**
|
|
|
|
```javascript
|
|
const request = require('supertest');
|
|
const app = require('../../index');
|
|
const { getDb } = require('../../dbPool');
|
|
|
|
describe('WorkReport API Integration Tests', () => {
|
|
let db;
|
|
let authToken;
|
|
|
|
beforeAll(async () => {
|
|
db = await getDb();
|
|
|
|
// 테스트용 인증 토큰 생성 (실제 로그인 API 호출 또는 JWT 직접 생성)
|
|
const loginResponse = await request(app)
|
|
.post('/api/auth/login')
|
|
.send({
|
|
username: 'test_admin',
|
|
password: 'test_password'
|
|
});
|
|
|
|
authToken = loginResponse.body.token;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
// 테스트 데이터 정리
|
|
await db.query('DELETE FROM daily_work_reports WHERE report_date = ?', ['2025-12-11']);
|
|
await db.end();
|
|
});
|
|
|
|
describe('POST /api/work-reports', () => {
|
|
it('작업 보고서를 생성해야 함', async () => {
|
|
const response = await request(app)
|
|
.post('/api/work-reports')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
report_date: '2025-12-11',
|
|
worker_id: 1,
|
|
project_id: 1,
|
|
work_type_id: 1,
|
|
work_hours: 8,
|
|
work_content: '테스트 작업'
|
|
})
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data).toHaveProperty('workReport_ids');
|
|
expect(response.body.data.workReport_ids).toHaveLength(1);
|
|
});
|
|
|
|
it('인증 토큰 없이 요청하면 401을 반환해야 함', async () => {
|
|
await request(app)
|
|
.post('/api/work-reports')
|
|
.send({
|
|
report_date: '2025-12-11',
|
|
worker_id: 1
|
|
})
|
|
.expect(401);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/work-reports/:date', () => {
|
|
it('날짜별 작업 보고서를 조회해야 함', async () => {
|
|
const response = await request(app)
|
|
.get('/api/work-reports/2025-12-11')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.expect(200);
|
|
|
|
expect(response.body.success).toBe(true);
|
|
expect(Array.isArray(response.body.data)).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 테스트 실행
|
|
|
|
### 기본 실행
|
|
|
|
```bash
|
|
# 모든 테스트 실행
|
|
npm test
|
|
|
|
# Watch 모드 (파일 변경 시 자동 재실행)
|
|
npm run test:watch
|
|
|
|
# 커버리지 리포트와 함께 실행
|
|
npm run test:coverage
|
|
|
|
# 특정 파일만 테스트
|
|
npm test -- tests/unit/services/workReportService.test.js
|
|
|
|
# 특정 describe 블록만 테스트
|
|
npm test -- --testNamePattern="createWorkReportService"
|
|
```
|
|
|
|
### 커버리지 확인
|
|
|
|
```bash
|
|
npm run test:coverage
|
|
|
|
# 커버리지 리포트 HTML 보기
|
|
open coverage/lcov-report/index.html
|
|
```
|
|
|
|
**목표 커버리지:**
|
|
- 서비스 레이어: 80% 이상
|
|
- 컨트롤러: 70% 이상
|
|
- 전체: 75% 이상
|
|
|
|
---
|
|
|
|
## 모범 사례
|
|
|
|
### 1. 테스트 이름 규칙
|
|
|
|
**Good:**
|
|
```javascript
|
|
it('should create work report when valid data is provided', async () => {});
|
|
it('should throw ValidationError when report_date is missing', async () => {});
|
|
it('should return 404 when work report not found', async () => {});
|
|
```
|
|
|
|
**Bad:**
|
|
```javascript
|
|
it('test1', async () => {});
|
|
it('works', async () => {});
|
|
```
|
|
|
|
### 2. AAA 패턴 (Arrange-Act-Assert)
|
|
|
|
```javascript
|
|
it('should ...', async () => {
|
|
// Arrange: 테스트 데이터 및 모킹 설정
|
|
const mockData = { ... };
|
|
service.method.mockResolvedValue(mockData);
|
|
|
|
// Act: 실제 테스트 대상 실행
|
|
const result = await functionUnderTest(params);
|
|
|
|
// Assert: 결과 검증
|
|
expect(result).toEqual(expectedResult);
|
|
expect(service.method).toHaveBeenCalledWith(expectedParams);
|
|
});
|
|
```
|
|
|
|
### 3. 독립적인 테스트
|
|
|
|
각 테스트는 다른 테스트에 의존하지 않아야 합니다.
|
|
|
|
```javascript
|
|
describe('Service', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks(); // 각 테스트 전에 모킹 초기화
|
|
});
|
|
|
|
it('test 1', () => { /* ... */ });
|
|
it('test 2', () => { /* ... */ }); // test 1의 영향을 받지 않음
|
|
});
|
|
```
|
|
|
|
### 4. 엣지 케이스 테스트
|
|
|
|
```javascript
|
|
describe('getWorkReportsByDateService', () => {
|
|
it('should handle empty date', async () => { /* ... */ });
|
|
it('should handle invalid date format', async () => { /* ... */ });
|
|
it('should handle null date', async () => { /* ... */ });
|
|
it('should handle future date', async () => { /* ... */ });
|
|
it('should return empty array when no reports found', async () => { /* ... */ });
|
|
});
|
|
```
|
|
|
|
### 5. 에러 케이스 테스트
|
|
|
|
```javascript
|
|
it('should throw ValidationError when required field is missing', async () => {
|
|
await expect(service.method(invalidData))
|
|
.rejects.toThrow(ValidationError);
|
|
|
|
await expect(service.method(invalidData))
|
|
.rejects.toThrow('보고서 데이터가 필요합니다');
|
|
});
|
|
```
|
|
|
|
### 6. 비동기 테스트
|
|
|
|
```javascript
|
|
// Good: async/await 사용
|
|
it('should ...', async () => {
|
|
const result = await asyncFunction();
|
|
expect(result).toBe(expected);
|
|
});
|
|
|
|
// Good: return Promise
|
|
it('should ...', () => {
|
|
return asyncFunction().then(result => {
|
|
expect(result).toBe(expected);
|
|
});
|
|
});
|
|
|
|
// Bad: Promise를 반환하지 않음
|
|
it('should ...', () => {
|
|
asyncFunction().then(result => {
|
|
expect(result).toBe(expected); // 실행되지 않을 수 있음!
|
|
});
|
|
});
|
|
```
|
|
|
|
### 7. 모킹 복원
|
|
|
|
```javascript
|
|
describe('Service', () => {
|
|
afterEach(() => {
|
|
jest.restoreAllMocks(); // 모든 모킹 복원
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 다음 단계
|
|
|
|
### Phase 1: 서비스 레이어 테스트 (우선)
|
|
```bash
|
|
tests/unit/services/
|
|
├── workReportService.test.js ⭐ 시작하기 좋음
|
|
├── attendanceService.test.js
|
|
├── dailyWorkReportService.test.js
|
|
├── workerService.test.js
|
|
├── projectService.test.js
|
|
├── issueTypeService.test.js
|
|
├── toolsService.test.js
|
|
├── dailyIssueReportService.test.js
|
|
├── uploadService.test.js
|
|
└── analysisService.test.js
|
|
```
|
|
|
|
### Phase 2: 컨트롤러 테스트
|
|
```bash
|
|
tests/unit/controllers/
|
|
├── workReportController.test.js
|
|
├── attendanceController.test.js
|
|
└── ...
|
|
```
|
|
|
|
### Phase 3: 통합 테스트
|
|
```bash
|
|
tests/integration/
|
|
├── workReport.test.js
|
|
├── attendance.test.js
|
|
└── ...
|
|
```
|
|
|
|
---
|
|
|
|
## 유용한 Jest 명령어
|
|
|
|
```bash
|
|
# 특정 파일만 watch
|
|
npm test -- --watch tests/unit/services/workReportService.test.js
|
|
|
|
# 실패한 테스트만 재실행
|
|
npm test -- --onlyFailures
|
|
|
|
# 병렬 실행 비활성화 (디버깅 시)
|
|
npm test -- --runInBand
|
|
|
|
# 상세 출력
|
|
npm test -- --verbose
|
|
|
|
# 특정 describe만 실행
|
|
npm test -- --testNamePattern="createWorkReportService"
|
|
```
|
|
|
|
---
|
|
|
|
## 트러블슈팅
|
|
|
|
### 문제 1: "Cannot find module"
|
|
```bash
|
|
# node_modules 재설치
|
|
rm -rf node_modules package-lock.json
|
|
npm install
|
|
```
|
|
|
|
### 문제 2: 타임아웃 에러
|
|
```javascript
|
|
// jest.config.js에서 타임아웃 증가
|
|
module.exports = {
|
|
testTimeout: 10000 // 10초
|
|
};
|
|
|
|
// 또는 개별 테스트에서
|
|
it('should ...', async () => {
|
|
// ...
|
|
}, 15000); // 15초
|
|
```
|
|
|
|
### 문제 3: DB 연결 오류 (통합 테스트)
|
|
```javascript
|
|
// tests/setup.js에서 테스트 DB 설정
|
|
process.env.DB_HOST = 'localhost';
|
|
process.env.DB_NAME = 'test_database';
|
|
```
|
|
|
|
---
|
|
|
|
## 참고 자료
|
|
|
|
- [Jest 공식 문서](https://jestjs.io/docs/getting-started)
|
|
- [Supertest GitHub](https://github.com/visionmedia/supertest)
|
|
- [Testing Best Practices](https://github.com/goldbergyoni/javascript-testing-best-practices)
|
|
|
|
---
|
|
|
|
**작성일**: 2025-12-11
|
|
**작성자**: TK-FB-Project Team
|
|
**버전**: 1.0
|