Skip to Content

테스트

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 / aftersetupTestDB() / 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 테스트에서는 ApplicationdisableListenServer: 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:test

package.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에 분리하여 각각 독립적으로 실행할 수 있습니다.


관련 문서

  • BootstrapApplication.run()disableListenServer 옵션
  • Your First API — Controller → Application → Entity 구조
Last updated on