테스트
ASAPJS는 Mocha + Chai + supertest 조합으로 테스트를 구성합니다. 인메모리 SQLite 데이터베이스를 사용하여 외부 DB 서버 없이 격리된 테스트 환경을 만들 수 있습니다.
Prerequisites:
mocha,chai,supertest,sqlite3,ts-node이 devDependencies에 설치되어 있어야 합니다.
테스트 의존성 설치
yarn add -D mocha chai supertest @types/mocha @types/chai @types/supertest ts-node sqlite3테스트 DB 셋업
테스트 환경에서는 인메모리 SQLite를 사용합니다. test/setup.ts 파일에서 테스트 DB를 초기화하고 정리하는 함수를 정의합니다.
// example/test/setup.ts
import 'reflect-metadata';
import { Sequelize } from 'sequelize-typescript';
import UsersTable from '../src/user/domain/entity/UsersTable';
import PostsTable from '../src/post/domain/entity/PostsTable';
let sequelize: Sequelize | null = null;
export async function setupTestDB() {
sequelize = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
models: [UsersTable, PostsTable],
});
await sequelize.sync({ force: true });
console.log('Test database initialized');
}
export async function teardownTestDB() {
if (sequelize) {
await sequelize.close();
console.log('Test database closed');
sequelize = null;
}
}핵심:
storage: ':memory:'로 인메모리 DB를 사용하고,sync({ force: true })로 매 테스트마다 테이블을 새로 생성합니다. 테스트 간 데이터 오염이 없습니다.
유닛 테스트 작성
유닛 테스트는 Application 레이어(비즈니스 로직)를 직접 호출하여 검증합니다. HTTP 서버를 띄우지 않고 순수 함수 호출로 테스트합니다.
// example/test/user.test.ts
import { expect } from 'chai';
import { setupTestDB, teardownTestDB } from './setup';
import UserApplication from '../src/user/application/UserApplication';
describe('UserApplication', () => {
let userApplication: UserApplication;
before(async () => {
await setupTestDB();
userApplication = new UserApplication();
});
after(async () => {
await teardownTestDB();
});
describe('회원 가입', () => {
it('새로운 사용자를 생성해야 한다', async () => {
const result = await userApplication.register({
email: 'test@example.com',
password: 'testpass123',
name: 'Test User',
});
expect(result).to.have.property('id');
expect(result.email).to.equal('test@example.com');
});
it('중복 이메일로 가입 시 에러를 발생시켜야 한다', async () => {
try {
await userApplication.register({
email: 'test@example.com',
password: 'testpass123',
name: 'Duplicate User',
});
expect.fail('Should have thrown an error');
} catch (error: any) {
expect(error.message).to.include('Email already exists');
}
});
});
describe('로그인', () => {
it('올바른 자격증명으로 토큰을 반환해야 한다', async () => {
const result = await userApplication.login({
email: 'test@example.com',
password: 'testpass123',
});
expect(result).to.have.property('accessToken');
});
});
});유닛 테스트 패턴 요약
| 패턴 | 설명 |
|---|---|
before / after | setupTestDB() / teardownTestDB()로 DB 생명주기 관리 |
expect(...).to.have.property() | 객체 속성 존재 여부 검증 |
expect(...).to.equal() | 값 일치 검증 |
expect.fail() in try/catch | 에러 발생 검증 (에러가 발생하지 않으면 실패) |
expect(...).to.be.an('array') | 타입 검증 |
E2E 테스트 작성
E2E 테스트는 실제 HTTP 요청을 보내 전체 요청/응답 흐름을 검증합니다. supertest를 사용하여 Express 앱에 직접 요청을 보냅니다.
E2E 테스트 앱 설정
E2E 테스트에서는 Application을 disableListenServer: true 옵션으로 실행하여 실제 포트 바인딩 없이 Express 앱 인스턴스를 얻습니다.
// example/test/e2e/app.ts
import 'reflect-metadata';
import path from 'path';
import { Application } from '@asapjs/core';
import { getSequelize } from '@asapjs/sequelize';
const JWT_SECRET = 'test-secret-key';
const config = {
name: 'ASAPJS Example E2E Test',
port: 0, // 랜덤 사용 가능한 포트
basePath: 'api',
extensions: ['@asapjs/sequelize'],
auth: {
jwt_access_token_secret: JWT_SECRET,
},
db: {
dialect: 'sqlite',
storage: ':memory:',
logging: false,
},
};
const srcPath = path.join(__dirname, '../../src');
export async function getTestApp() {
const appInstance = new Application(srcPath, config);
const expressApp = await appInstance.run(() => {}, { disableListenServer: true });
const sequelize = await getSequelize();
await sequelize.sync({ force: true });
return expressApp;
}
disableListenServer: true: HTTP 서버를 생성하되listen()을 호출하지 않습니다. supertest가 내부적으로 서버를 관리하므로 포트 충돌이 발생하지 않습니다.
port: 0: 만약 서버를 직접 띄울 경우, OS가 사용 가능한 랜덤 포트를 자동으로 할당합니다.
E2E 테스트 코드
// example/test/e2e/user-flow.test.ts
import { expect } from 'chai';
import request from 'supertest';
import { getTestApp } from './app';
describe('User E2E Flow', () => {
let app: any;
let accessToken: string;
before(async function () {
this.timeout(20000); // 앱 초기화에 시간이 걸릴 수 있음
app = await getTestApp();
});
describe('POST /api/users/register', () => {
it('새 사용자를 등록해야 한다', async () => {
const res = await request(app)
.post('/api/users/register')
.send({
email: 'e2etest@example.com',
password: 'testpass123',
name: 'E2E Test User',
})
.expect('Content-Type', /json/)
.expect(200);
expect(res.body.email).to.equal('e2etest@example.com');
});
});
describe('POST /api/users/login', () => {
it('로그인 후 토큰을 반환해야 한다', async () => {
const res = await request(app)
.post('/api/users/login')
.send({
email: 'e2etest@example.com',
password: 'testpass123',
})
.expect(200);
expect(res.body).to.have.property('accessToken');
accessToken = res.body.accessToken;
});
});
describe('GET /api/users/me (인증 필요)', () => {
it('Bearer 토큰으로 내 정보를 조회해야 한다', async () => {
const res = await request(app)
.get('/api/users/me')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200);
expect(res.body.email).to.equal('e2etest@example.com');
});
it('토큰 없이 요청 시 403을 반환해야 한다', async () => {
await request(app)
.get('/api/users/me')
.expect(403);
});
});
});E2E 테스트 패턴 요약
| 패턴 | 설명 |
|---|---|
request(app).post(path).send(body) | POST 요청 전송 |
.set('Authorization', 'Bearer ' + accessToken) | JWT 인증 헤더 설정 |
.expect('Content-Type', /json/) | 응답 Content-Type 검증 |
.expect(200) | HTTP 상태 코드 검증 |
res.body | 응답 본문 접근 |
테스트 실행 명령어
# 유닛 테스트 실행
cd example && yarn test
# E2E 테스트 실행
cd example && yarn test:e2e
# 유닛 + E2E 전체 실행
cd example && yarn test:all
# Watch 모드 (파일 변경 시 자동 재실행)
cd example && yarn test:watch
# 단일 테스트 파일 실행
cd example && npx mocha --require ts-node/register 'test/user.test.ts' --timeout 10000
# 루트에서 example 테스트 실행
yarn example:testpackage.json 테스트 스크립트
{
"scripts": {
"test": "mocha --require ts-node/register 'test/*.test.ts' --timeout 10000",
"test:watch": "mocha --watch --require ts-node/register 'test/**/*.test.ts' --timeout 10000",
"test:e2e": "mocha --require ts-node/register 'test/e2e/**/*.test.ts' --timeout 10000",
"test:all": "yarn test && yarn test:e2e"
}
}Mocha 설정
프로젝트 루트에 .mocharc.json을 생성하면 CLI 옵션을 생략할 수 있습니다.
{
"require": ["ts-node/register"],
"timeout": 10000,
"spec": "test/*.test.ts"
}테스트 파일 구조
example/
test/
setup.ts # 인메모리 SQLite DB 초기화
user.test.ts # UserApplication 유닛 테스트
post.test.ts # PostApplication 유닛 테스트
e2e/
app.ts # E2E 테스트용 Application 설정
user-flow.test.ts # 사용자 흐름 E2E 테스트Tip: 유닛 테스트는
test/*.test.ts에, E2E 테스트는test/e2e/*.test.ts에 분리하여 각각 독립적으로 실행할 수 있습니다.
관련 문서
- Bootstrap —
Application.run()과disableListenServer옵션 - Your First API — Controller → Application → Entity 구조