Node.js 테스트 | Jest, Mocha, Supertest 완벽 가이드

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();
});

정리

핵심 요약

  1. Jest: 올인원 테스트 프레임워크 (권장)
  2. Mocha + Chai: 유연한 조합
  3. Supertest: Express API 테스트
  4. Mock/Spy: 외부 의존성 격리
  5. 커버리지: 80% 이상 목표
  6. TDD: 테스트 먼저, 구현 나중

테스트 프레임워크 비교

프레임워크장점단점
Jest올인원, 빠름, 스냅샷무거움
Mocha유연, 가벼움추가 라이브러리 필요
AVA병렬 실행생태계 작음

테스트 베스트 프랙티스

  • ✅ 테스트는 독립적이어야 함
  • ✅ 하나의 테스트는 하나만 검증
  • ✅ 의미 있는 테스트 이름
  • ✅ AAA 패턴 (Arrange, Act, Assert)
  • ✅ 실패 케이스도 테스트
  • ✅ 외부 의존성은 Mock
  • ✅ 테스트 데이터는 최소화

다음 단계

  • Node.js 배포
  • Node.js 성능 최적화
  • Node.js CI/CD

추천 학습 자료

공식 문서:

도구:


관련 글

  • Node.js 시작하기 | 설치, 설정, Hello World