Node.js 테스트 | Jest, Mocha, Supertest 완벽 가이드
이 글의 핵심
Node.js 테스트에 대한 실전 가이드입니다. Jest, Mocha, Supertest 완벽 가이드 등을 예제와 함께 상세히 설명합니다.
들어가며
테스트의 중요성
테스트는 코드가 의도대로 동작하는지 확인하는 과정입니다.
자동 테스트는 리팩터링할 때 안전벨트입니다. 같은 입력에 같은 출력이 나오는지 기계가 확인해 주면, 비동기·Express 라우트처럼 손으로 curl만 반복하기 어려운 경로도 회귀를 막을 수 있습니다.
단위 테스트는 모든 언어에서 중요합니다. Python에서 pytest·CI·테스트 디렉터리, C++의 Google Test, Go의 go test, Rust의 cargo test는 각각의 생태계에서 표준에 가깝습니다. CI/CD·배포와 연결하려면 GitHub Actions로 Node.js CI/CD, Docker Compose 프로덕션, C++ Docker·배포 이미지를 함께 보세요.
장점:
- ✅ 버그 조기 발견: 배포 전 문제 발견
- ✅ 리팩토링 안전성: 변경 시 기존 기능 보장
- ✅ 문서화: 테스트가 사용법을 보여줌
- ✅ 자신감: 코드 변경에 대한 확신
- ✅ 유지보수: 장기적으로 시간 절약
테스트 종류:
- 단위 테스트 (Unit Test): 개별 함수/메서드
- 통합 테스트 (Integration Test): 여러 모듈 조합
- E2E 테스트 (End-to-End): 전체 시스템
1. Jest
설치
npm install --save-dev jest
package.json 설정:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
첫 테스트
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = { add, subtract };
// math.test.js
const { add, subtract } = require('./math');
describe('Math 함수', () => {
test('add: 두 숫자를 더한다', () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
expect(add(0, 0)).toBe(0);
});
test('subtract: 두 숫자를 뺀다', () => {
expect(subtract(5, 3)).toBe(2);
expect(subtract(1, 1)).toBe(0);
expect(subtract(0, 5)).toBe(-5);
});
});
실행:
npm test
Matchers (검증)
describe('Matchers 예제', () => {
test('동등성', () => {
expect(2 + 2).toBe(4); // ===
expect({ name: '홍길동' }).toEqual({ name: '홍길동' }); // 깊은 비교
});
test('참/거짓', () => {
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('hello').toBeDefined();
});
test('숫자', () => {
expect(10).toBeGreaterThan(5);
expect(10).toBeGreaterThanOrEqual(10);
expect(5).toBeLessThan(10);
expect(0.1 + 0.2).toBeCloseTo(0.3); // 부동소수점
});
test('문자열', () => {
expect('hello world').toMatch(/world/);
expect('hello').toContain('ell');
});
test('배열/객체', () => {
const arr = ['apple', 'banana', 'cherry'];
expect(arr).toContain('banana');
expect(arr).toHaveLength(3);
const obj = { name: '홍길동', age: 25 };
expect(obj).toHaveProperty('name');
expect(obj).toHaveProperty('age', 25);
});
test('예외', () => {
expect(() => {
throw new Error('에러!');
}).toThrow('에러!');
});
});
2. 비동기 테스트
Promise 테스트
// api.js
async function fetchUser(id) {
const response = await fetch(`https://api.example.com/users/${id}`);
return response.json();
}
module.exports = { fetchUser };
// api.test.js
const { fetchUser } = require('./api');
describe('API 테스트', () => {
test('사용자를 가져온다', async () => {
const user = await fetchUser(1);
expect(user).toHaveProperty('id', 1);
expect(user).toHaveProperty('name');
});
test('존재하지 않는 사용자', async () => {
await expect(fetchUser(999)).rejects.toThrow();
});
});
Setup/Teardown
describe('데이터베이스 테스트', () => {
// 모든 테스트 전에 한 번 실행
beforeAll(async () => {
await connectDatabase();
});
// 각 테스트 전에 실행
beforeEach(async () => {
await clearDatabase();
});
// 각 테스트 후에 실행
afterEach(async () => {
// 정리 작업
});
// 모든 테스트 후에 한 번 실행
afterAll(async () => {
await closeDatabase();
});
test('사용자 생성', async () => {
const user = await User.create({ name: '홍길동' });
expect(user).toHaveProperty('_id');
});
});
3. Mock과 Spy
Mock 함수
describe('Mock 함수', () => {
test('함수 호출 확인', () => {
const mockFn = jest.fn();
mockFn('hello');
mockFn('world');
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('hello');
expect(mockFn).toHaveBeenLastCalledWith('world');
});
test('반환값 설정', () => {
const mockFn = jest.fn();
mockFn.mockReturnValue(42);
expect(mockFn()).toBe(42);
mockFn.mockReturnValueOnce(1)
.mockReturnValueOnce(2)
.mockReturnValue(3);
expect(mockFn()).toBe(1);
expect(mockFn()).toBe(2);
expect(mockFn()).toBe(3);
expect(mockFn()).toBe(3);
});
});
모듈 Mock
// user.js
const axios = require('axios');
async function getUser(id) {
const response = await axios.get(`https://api.example.com/users/${id}`);
return response.data;
}
module.exports = { getUser };
// user.test.js
const axios = require('axios');
const { getUser } = require('./user');
jest.mock('axios');
describe('getUser', () => {
test('사용자를 가져온다', async () => {
const mockUser = { id: 1, name: '홍길동' };
axios.get.mockResolvedValue({ data: mockUser });
const user = await getUser(1);
expect(axios.get).toHaveBeenCalledWith('https://api.example.com/users/1');
expect(user).toEqual(mockUser);
});
test('에러 처리', async () => {
axios.get.mockRejectedValue(new Error('Network Error'));
await expect(getUser(1)).rejects.toThrow('Network Error');
});
});
Spy
describe('Spy 예제', () => {
test('메서드 호출 감시', () => {
const obj = {
method: () => 'original'
};
const spy = jest.spyOn(obj, 'method');
obj.method();
expect(spy).toHaveBeenCalled();
spy.mockRestore(); // 원래대로 복원
});
});
4. API 테스트 (Supertest)
설치
npm install --save-dev supertest
Express 앱 테스트
// app.js
const express = require('express');
const app = express();
app.use(express.json());
app.get('/api/users', (req, res) => {
res.json({ users: [] });
});
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: '이름과 이메일이 필요합니다' });
}
res.status(201).json({ id: 1, name, email });
});
module.exports = app; // 서버 시작하지 않고 export
// app.test.js
const request = require('supertest');
const app = require('./app');
describe('API 테스트', () => {
describe('GET /api/users', () => {
test('모든 사용자를 반환한다', async () => {
const response = await request(app)
.get('/api/users')
.expect(200)
.expect('Content-Type', /json/);
expect(response.body).toHaveProperty('users');
expect(Array.isArray(response.body.users)).toBe(true);
});
});
describe('POST /api/users', () => {
test('사용자를 생성한다', async () => {
const newUser = {
name: '홍길동',
email: '[email protected]'
};
const response = await request(app)
.post('/api/users')
.send(newUser)
.expect(201)
.expect('Content-Type', /json/);
expect(response.body).toMatchObject(newUser);
expect(response.body).toHaveProperty('id');
});
test('유효하지 않은 입력은 400을 반환한다', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: '홍길동' }) // email 누락
.expect(400);
expect(response.body).toHaveProperty('error');
});
});
});
5. 데이터베이스 테스트
MongoDB 테스트 설정
// test/setup.js
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
await mongoose.connect(mongoUri);
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
afterEach(async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany();
}
});
jest.config.js:
module.exports = {
testEnvironment: 'node',
setupFilesAfterEnv: ['<rootDir>/test/setup.js'],
coveragePathIgnorePatterns: ['/node_modules/']
};
모델 테스트
// models/User.test.js
const User = require('../models/User');
describe('User 모델', () => {
test('사용자를 생성한다', async () => {
const userData = {
name: '홍길동',
email: '[email protected]',
password: 'password123'
};
const user = await User.create(userData);
expect(user).toHaveProperty('_id');
expect(user.name).toBe(userData.name);
expect(user.email).toBe(userData.email);
});
test('이메일 중복은 에러를 발생시킨다', async () => {
await User.create({
name: '홍길동',
email: '[email protected]',
password: 'password123'
});
await expect(User.create({
name: '김철수',
email: '[email protected]', // 중복
password: 'password456'
})).rejects.toThrow();
});
test('필수 필드가 없으면 에러', async () => {
await expect(User.create({
name: '홍길동'
// email, password 누락
})).rejects.toThrow();
});
});
6. 통합 테스트
REST API 통합 테스트
// test/integration/users.test.js
const request = require('supertest');
const app = require('../../app');
const User = require('../../models/User');
describe('Users API 통합 테스트', () => {
describe('POST /api/users', () => {
test('사용자를 생성한다', async () => {
const userData = {
name: '홍길동',
email: '[email protected]',
password: 'password123'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe(userData.name);
// 데이터베이스 확인
const user = await User.findById(response.body.id);
expect(user).toBeTruthy();
expect(user.email).toBe(userData.email);
});
});
describe('GET /api/users/:id', () => {
test('사용자를 조회한다', async () => {
// 테스트 데이터 생성
const user = await User.create({
name: '홍길동',
email: '[email protected]',
password: 'password123'
});
const response = await request(app)
.get(`/api/users/${user._id}`)
.expect(200);
expect(response.body.name).toBe(user.name);
expect(response.body.email).toBe(user.email);
});
test('존재하지 않는 사용자는 404를 반환한다', async () => {
const response = await request(app)
.get('/api/users/507f1f77bcf86cd799439011')
.expect(404);
expect(response.body).toHaveProperty('error');
});
});
});
인증 테스트
// test/integration/auth.test.js
const request = require('supertest');
const app = require('../../app');
const User = require('../../models/User');
const bcrypt = require('bcrypt');
describe('Auth API', () => {
describe('POST /auth/login', () => {
test('올바른 자격증명으로 로그인', async () => {
// 테스트 사용자 생성
const password = 'password123';
const hashedPassword = await bcrypt.hash(password, 10);
await User.create({
name: '홍길동',
email: '[email protected]',
password: hashedPassword
});
const response = await request(app)
.post('/auth/login')
.send({
email: '[email protected]',
password: password
})
.expect(200);
expect(response.body).toHaveProperty('token');
expect(response.body.user.email).toBe('[email protected]');
});
test('잘못된 비밀번호는 401을 반환한다', async () => {
const hashedPassword = await bcrypt.hash('password123', 10);
await User.create({
name: '홍길동',
email: '[email protected]',
password: hashedPassword
});
await request(app)
.post('/auth/login')
.send({
email: '[email protected]',
password: 'wrongpassword'
})
.expect(401);
});
});
describe('GET /api/profile', () => {
test('인증된 사용자는 프로필을 조회할 수 있다', async () => {
// 사용자 생성 및 로그인
const user = await User.create({
name: '홍길동',
email: '[email protected]',
password: await bcrypt.hash('password123', 10)
});
const loginResponse = await request(app)
.post('/auth/login')
.send({
email: '[email protected]',
password: 'password123'
});
const token = loginResponse.body.token;
// 프로필 조회
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body.user.email).toBe('[email protected]');
});
test('토큰 없이는 401을 반환한다', async () => {
await request(app)
.get('/api/profile')
.expect(401);
});
});
});
7. Mocha + Chai
설치
npm install --save-dev mocha chai
package.json:
{
"scripts": {
"test": "mocha"
}
}
기본 테스트
// test/math.test.js
const { expect } = require('chai');
const { add, subtract } = require('../math');
describe('Math 함수', () => {
describe('add()', () => {
it('두 숫자를 더한다', () => {
expect(add(2, 3)).to.equal(5);
expect(add(-1, 1)).to.equal(0);
});
});
describe('subtract()', () => {
it('두 숫자를 뺀다', () => {
expect(subtract(5, 3)).to.equal(2);
});
});
});
Chai Assertions
const { expect } = require('chai');
describe('Chai Assertions', () => {
it('동등성', () => {
expect(2 + 2).to.equal(4);
expect({ name: '홍길동' }).to.deep.equal({ name: '홍길동' });
});
it('타입', () => {
expect('hello').to.be.a('string');
expect(123).to.be.a('number');
expect([]).to.be.an('array');
});
it('포함', () => {
expect([1, 2, 3]).to.include(2);
expect('hello world').to.include('world');
expect({ name: '홍길동', age: 25 }).to.include({ name: '홍길동' });
});
it('길이', () => {
expect([1, 2, 3]).to.have.lengthOf(3);
expect('hello').to.have.lengthOf(5);
});
it('속성', () => {
const obj = { name: '홍길동', age: 25 };
expect(obj).to.have.property('name');
expect(obj).to.have.property('age', 25);
});
});
8. 테스트 커버리지
Jest 커버리지
npm test -- --coverage
출력:
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 85.71 | 75 | 100 | 85.71 |
math.js | 85.71 | 75 | 100 | 85.71 | 12-15
----------|---------|----------|---------|---------|-------------------
jest.config.js:
module.exports = {
collectCoverageFrom: [
'src/**/*.js',
'!src/**/*.test.js',
'!src/index.js'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
9. 실전 예제
예제 1: 사용자 서비스 테스트
// services/userService.js
const User = require('../models/User');
const bcrypt = require('bcrypt');
class UserService {
async createUser(userData) {
const { email, password, name } = userData;
// 중복 확인
const existing = await User.findOne({ email });
if (existing) {
throw new Error('이미 존재하는 이메일입니다');
}
// 비밀번호 해싱
const hashedPassword = await bcrypt.hash(password, 10);
// 사용자 생성
const user = await User.create({
email,
password: hashedPassword,
name
});
return user;
}
async getUserById(id) {
const user = await User.findById(id).select('-password');
if (!user) {
throw new Error('사용자를 찾을 수 없습니다');
}
return user;
}
async updateUser(id, updates) {
const user = await User.findByIdAndUpdate(
id,
updates,
{ new: true, runValidators: true }
).select('-password');
if (!user) {
throw new Error('사용자를 찾을 수 없습니다');
}
return user;
}
async deleteUser(id) {
const user = await User.findByIdAndDelete(id);
if (!user) {
throw new Error('사용자를 찾을 수 없습니다');
}
return user;
}
}
module.exports = new UserService();
// services/userService.test.js
const userService = require('./userService');
const User = require('../models/User');
describe('UserService', () => {
describe('createUser', () => {
test('사용자를 생성한다', async () => {
const userData = {
name: '홍길동',
email: '[email protected]',
password: 'password123'
};
const user = await userService.createUser(userData);
expect(user).toHaveProperty('_id');
expect(user.name).toBe(userData.name);
expect(user.email).toBe(userData.email);
expect(user.password).not.toBe(userData.password); // 해싱됨
});
test('중복 이메일은 에러를 발생시킨다', async () => {
await userService.createUser({
name: '홍길동',
email: '[email protected]',
password: 'password123'
});
await expect(userService.createUser({
name: '김철수',
email: '[email protected]',
password: 'password456'
})).rejects.toThrow('이미 존재하는 이메일입니다');
});
});
describe('getUserById', () => {
test('사용자를 조회한다', async () => {
const created = await User.create({
name: '홍길동',
email: '[email protected]',
password: 'hashed'
});
const user = await userService.getUserById(created._id);
expect(user._id.toString()).toBe(created._id.toString());
expect(user).not.toHaveProperty('password'); // 비밀번호 제외
});
test('존재하지 않는 사용자는 에러', async () => {
await expect(
userService.getUserById('507f1f77bcf86cd799439011')
).rejects.toThrow('사용자를 찾을 수 없습니다');
});
});
});
예제 2: 인증 테스트
// test/integration/auth.test.js
const request = require('supertest');
const app = require('../../app');
const User = require('../../models/User');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
describe('인증 API', () => {
let testUser;
let authToken;
beforeEach(async () => {
// 테스트 사용자 생성
testUser = await User.create({
name: '홍길동',
email: '[email protected]',
password: await bcrypt.hash('password123', 10),
role: 'user'
});
});
describe('POST /auth/register', () => {
test('회원가입 성공', async () => {
const response = await request(app)
.post('/auth/register')
.send({
name: '김철수',
email: '[email protected]',
password: 'password123'
})
.expect(201);
expect(response.body).toHaveProperty('user');
expect(response.body.user.email).toBe('[email protected]');
// 비밀번호가 해싱되었는지 확인
const user = await User.findOne({ email: '[email protected]' });
expect(user.password).not.toBe('password123');
});
});
describe('POST /auth/login', () => {
test('로그인 성공', async () => {
const response = await request(app)
.post('/auth/login')
.send({
email: '[email protected]',
password: 'password123'
})
.expect(200);
expect(response.body).toHaveProperty('token');
expect(response.body.user.email).toBe('[email protected]');
// 토큰 검증
const decoded = jwt.verify(response.body.token, process.env.JWT_SECRET);
expect(decoded.email).toBe('[email protected]');
authToken = response.body.token;
});
test('잘못된 비밀번호', async () => {
await request(app)
.post('/auth/login')
.send({
email: '[email protected]',
password: 'wrongpassword'
})
.expect(401);
});
});
describe('GET /api/profile', () => {
beforeEach(async () => {
// 로그인하여 토큰 획득
const response = await request(app)
.post('/auth/login')
.send({
email: '[email protected]',
password: 'password123'
});
authToken = response.body.token;
});
test('인증된 사용자는 프로필 조회 가능', async () => {
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(response.body.user.email).toBe('[email protected]');
});
test('토큰 없이는 401', async () => {
await request(app)
.get('/api/profile')
.expect(401);
});
test('유효하지 않은 토큰은 401', async () => {
await request(app)
.get('/api/profile')
.set('Authorization', 'Bearer invalid-token')
.expect(401);
});
});
});
10. 테스트 더블 (Test Doubles)
Mock
// emailService.js
const nodemailer = require('nodemailer');
async function sendEmail(to, subject, text) {
const transporter = nodemailer.createTransport({
host: 'smtp.example.com',
port: 587,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
}
});
await transporter.sendMail({ to, subject, text });
}
module.exports = { sendEmail };
// emailService.test.js
const { sendEmail } = require('./emailService');
const nodemailer = require('nodemailer');
jest.mock('nodemailer');
describe('sendEmail', () => {
test('이메일을 발송한다', async () => {
const mockSendMail = jest.fn().mockResolvedValue({ messageId: '123' });
const mockTransporter = {
sendMail: mockSendMail
};
nodemailer.createTransport.mockReturnValue(mockTransporter);
await sendEmail('[email protected]', '제목', '내용');
expect(mockSendMail).toHaveBeenCalledWith({
to: '[email protected]',
subject: '제목',
text: '내용'
});
});
});
Stub
// paymentService.js
const axios = require('axios');
async function processPayment(amount) {
const response = await axios.post('https://payment-api.com/charge', {
amount
});
return response.data;
}
module.exports = { processPayment };
// paymentService.test.js
const { processPayment } = require('./paymentService');
const axios = require('axios');
jest.mock('axios');
describe('processPayment', () => {
test('결제를 처리한다', async () => {
const mockResponse = {
data: {
success: true,
transactionId: 'txn_123'
}
};
axios.post.mockResolvedValue(mockResponse);
const result = await processPayment(10000);
expect(result.success).toBe(true);
expect(result.transactionId).toBe('txn_123');
});
});
11. 자주 발생하는 문제
문제 1: 비동기 테스트 타임아웃
에러:
Timeout - Async callback was not invoked within the 5000 ms timeout
해결:
// 타임아웃 증가
test('느린 작업', async () => {
await slowOperation();
}, 10000); // 10초
// jest.config.js
module.exports = {
testTimeout: 10000
};
문제 2: 테스트 간 간섭
// ❌ 전역 상태 공유
let users = [];
test('사용자 추가', () => {
users.push({ name: '홍길동' });
expect(users).toHaveLength(1);
});
test('사용자 목록', () => {
expect(users).toHaveLength(0); // 실패! (이전 테스트 영향)
});
// ✅ beforeEach로 초기화
let users;
beforeEach(() => {
users = [];
});
test('사용자 추가', () => {
users.push({ name: '홍길동' });
expect(users).toHaveLength(1);
});
test('사용자 목록', () => {
expect(users).toHaveLength(0); // 성공!
});
문제 3: 데이터베이스 정리 누락
// ✅ afterEach로 정리
afterEach(async () => {
await User.deleteMany({});
await Post.deleteMany({});
});
// ✅ 또는 트랜잭션 롤백 (PostgreSQL)
let connection;
beforeEach(async () => {
connection = await pool.getConnection();
await connection.beginTransaction();
});
afterEach(async () => {
await connection.rollback();
connection.release();
});
12. 실전 팁
테스트 구조화
test/
├── unit/
│ ├── models/
│ │ └── User.test.js
│ ├── services/
│ │ └── userService.test.js
│ └── utils/
│ └── validator.test.js
├── integration/
│ ├── auth.test.js
│ └── users.test.js
├── e2e/
│ └── userFlow.test.js
└── setup.js
테스트 헬퍼
// test/helpers.js
const User = require('../models/User');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
async function createTestUser(overrides = {}) {
const defaultUser = {
name: '테스트 사용자',
email: '[email protected]',
password: await bcrypt.hash('password123', 10),
role: 'user'
};
return await User.create({ ...defaultUser, ...overrides });
}
function generateTestToken(user) {
return jwt.sign(
{ id: user._id, email: user.email, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
}
module.exports = {
createTestUser,
generateTestToken
};
// 사용
const { createTestUser, generateTestToken } = require('./helpers');
test('사용자 조회', async () => {
const user = await createTestUser({ name: '홍길동' });
const token = generateTestToken(user);
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${token}`)
.expect(200);
});
TDD (Test-Driven Development)
// 1. 실패하는 테스트 작성
describe('calculateDiscount', () => {
test('10% 할인을 계산한다', () => {
expect(calculateDiscount(10000, 10)).toBe(9000);
});
});
// 2. 최소한의 코드로 테스트 통과
function calculateDiscount(price, discountPercent) {
return price - (price * discountPercent / 100);
}
// 3. 리팩토링
function calculateDiscount(price, discountPercent) {
if (price < 0 || discountPercent < 0 || discountPercent > 100) {
throw new Error('유효하지 않은 입력');
}
return price * (1 - discountPercent / 100);
}
// 4. 추가 테스트
test('유효하지 않은 입력은 에러', () => {
expect(() => calculateDiscount(-100, 10)).toThrow();
expect(() => calculateDiscount(100, -10)).toThrow();
expect(() => calculateDiscount(100, 150)).toThrow();
});
정리
핵심 요약
- Jest: 올인원 테스트 프레임워크 (권장)
- Mocha + Chai: 유연한 조합
- Supertest: Express API 테스트
- Mock/Spy: 외부 의존성 격리
- 커버리지: 80% 이상 목표
- TDD: 테스트 먼저, 구현 나중
테스트 프레임워크 비교
| 프레임워크 | 장점 | 단점 |
|---|---|---|
| Jest | 올인원, 빠름, 스냅샷 | 무거움 |
| Mocha | 유연, 가벼움 | 추가 라이브러리 필요 |
| AVA | 병렬 실행 | 생태계 작음 |
테스트 베스트 프랙티스
- ✅ 테스트는 독립적이어야 함
- ✅ 하나의 테스트는 하나만 검증
- ✅ 의미 있는 테스트 이름
- ✅ AAA 패턴 (Arrange, Act, Assert)
- ✅ 실패 케이스도 테스트
- ✅ 외부 의존성은 Mock
- ✅ 테스트 데이터는 최소화
다음 단계
- Node.js 배포
- Node.js 성능 최적화
- Node.js CI/CD
추천 학습 자료
공식 문서:
도구:
- mongodb-memory-server
- sinon - Mock/Stub/Spy
관련 글
- Node.js 시작하기 | 설치, 설정, Hello World