Jest와 Supertest로 Node.js E2E 테스트 구성
Jest와 Supertest를 이용한 Node.js 기반 API E2E 테스트 설정과 실행 흐름을 예제 코드와 함께 설명하는 구성
목차
소개
이 글은 Node.js 애플리케이션을 대상으로 Jest와 Supertest로 E2E(종단간) 테스트를 구성하는 방법을 설명한다. 처음 접하는 개발자도 따라 할 수 있도록 설치, 서버 구성, 테스트 작성, 실행 순서를 차근차근 제시한다. 핵심은 실제 서버 동작을 흉내 내지 않고도 API 흐름을 검증하는 데 있다.
왜 E2E 테스트가 필요한가
기능 검증 관점
E2E 테스트는 클라이언트 요청부터 데이터 응답까지 전체 흐름을 확인한다. 단위 테스트로는 놓치기 쉬운 라우팅, 미들웨어, 인증 흐름을 검증할 수 있다.
통합 안정성 확보
라우터와 데이터베이스 연결, 직렬화 포맷 등이 함께 동작하는지 확인한다. 배포 전 회귀를 줄이는 데 효과적이다.
사전 요구사항
- Node.js LTS 버전
- 간단한 Express 기반 API(또는 유사 프레임워크)
- npm 또는 yarn 사용 환경
프로젝트 초기 설정
프로젝트 폴더를 만들고 패키지를 설치한다. 테스트 러너로 Jest를 사용하고, 요청 시뮬레이션은 Supertest로 처리한다.
npm init -y
npm install express
npm install --save-dev jest supertest nodemon
간단한 Express 서버 예제
테스트 편의를 위해 서버 코드는 app 객체를 모듈로 내보낸다. 이렇게 하면 Supertest가 직접 app을 가져와 테스트할 수 있다.
// server.js
const express = require('express');
const app = express();
app.use(express.json());
let items = [{ id: 1, name: 'item1' }];
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok' });
});
app.get('/items', (req, res) => {
res.json(items);
});
app.post('/items', (req, res) => {
const { name } = req.body;
const id = items.length + 1;
const newItem = { id, name };
items.push(newItem);
res.status(201).json(newItem);
});
module.exports = app;
// index.js (실제 실행용)
// const app = require('./server');
// const port = process.env.PORT || 3000;
// app.listen(port, () => console.log(`Listening ${port}`));
Jest 설정
기본 Jest 설정으로도 충분하다. package.json에 테스트 스크립트를 추가한다. 테스트 실행 시 환경 변수를 분리하거나 별도 설정 파일을 둘 수 있다.
"scripts": {
"start": "node index.js",
"test": "jest --runInBand"
}
Supertest를 이용한 E2E 테스트 예제
Supertest는 내부적으로 HTTP 요청을 만들어 app을 호출한다. 별도 서버를 띄우지 않아도 된다. 테스트는 독립적으로 실행될 수 있도록 상태 초기화가 필요하다.
// e2e.test.js
const request = require('supertest');
const app = require('./server');
describe('API 통합 테스트 Node', () => {
beforeEach(() => {
// 서버 상태 초기화. 실제 환경에선 DB를 트랜잭션으로 롤백하거나
// 테스트 전용 DB를 사용한다.
// 여기서는 간단히 모의 데이터만 재할당.
const module = require('./server');
module.locals = module.locals || {};
});
test('GET /health 응답 확인', async () => {
const res = await request(app).get('/health');
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('status', 'ok');
});
test('GET /items 기본 목록', async () => {
const res = await request(app).get('/items');
expect(res.statusCode).toBe(200);
expect(Array.isArray(res.body)).toBe(true);
});
test('POST /items 항목 추가', async () => {
const payload = { name: 'new-item' };
const res = await request(app).post('/items').send(payload);
expect(res.statusCode).toBe(201);
expect(res.body).toHaveProperty('id');
expect(res.body).toHaveProperty('name', 'new-item');
const list = await request(app).get('/items');
expect(list.body.some(i => i.name === 'new-item')).toBe(true);
});
});
실행과 결과 확인
npm test로 실행한다. --runInBand 옵션은 테스트를 순차 실행해 포트 충돌이나 전역 상태 문제를 줄인다. 실패 시에는 로그와 응답 바디를 확인해 원인을 좁힌다.
베스트 프랙티스
- 테스트 전용 데이터베이스를 사용하거나 트랜잭션을 롤백한다.
- 테스트는 독립적으로 설계한다. 서로 의존하면 신뢰도가 떨어진다.
- 외부 API 호출은 목(mock) 처리해 네트워크 불안정성을 제거한다.
- 테스트 실행 속도가 느려지면 테스트 범위를 나눠 병렬화나 레이어별 테스트로 보완한다.
문제 해결 요령
테스트가 간헐적으로 실패하면 전역 상태를 의심한다. 모듈 캐시, 전역 변수, 비동기 정리 미흡 등이 원인이다. Jest의 setupFiles 또는 afterAll 훅을 활용해 정리 로직을 추가한다.
요약
Jest와 Supertest를 조합하면 Node.js API에 대해 현실적인 E2E 검증을 할 수 있다. 간단한 Express 예제와 테스트 코드를 통해 기본 흐름을 익힌 뒤, 데이터베이스와 인증이 포함된 더 복잡한 시나리오로 확장하면 된다. 이 구성은 회귀 방지와 배포 신뢰도 향상에 유용하다.