[NestJS] Testing (Jest, Supertest, e2e)
정의
NestJS Testing 은 Jest (기본) + Supertest 조합으로 unit test 와 e2e test 를 지원합니다. @nestjs/testing 의 Test.createTestingModule 로 module 을 격리된 형태로 만들고, provider 를 mock 으로 교체합니다.
Unit Test 기본
// users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { UsersRepository } from './users.repository';
describe('UsersService', () => {
let service: UsersService;
let repo: jest.Mocked<UsersRepository>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: UsersRepository,
useValue: {
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
},
},
],
}).compile();
service = module.get<UsersService>(UsersService);
repo = module.get(UsersRepository);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('findAll returns users', async () => {
repo.findAll.mockResolvedValue([{id: 1, name: 'kim'}]);
const result = await service.findAll();
expect(result).toEqual([{id: 1, name: 'kim'}]);
expect(repo.findAll).toHaveBeenCalledTimes(1);
});
it('create throws on duplicate email', async () => {
repo.create.mockRejectedValue(new UserAlreadyExistsError('a@b.com'));
await expect(
service.create({email: 'a@b.com', name: 'kim'})
).rejects.toThrow(UserAlreadyExistsError);
});
});
Controller Test
// users.controller.spec.ts
describe('UsersController', () => {
let controller: UsersController;
let service: jest.Mocked<UsersService>;
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useValue: {
findAll: jest.fn(),
findOne: jest.fn(),
},
},
],
}).compile();
controller = module.get(UsersController);
service = module.get(UsersService);
});
it('GET /users returns all users', async () => {
service.findAll.mockResolvedValue([{id: 1, name: 'kim'}]);
const result = await controller.findAll();
expect(result).toHaveLength(1);
});
});
Controller test 는 unit test 지만 실제 HTTP 는 안 거침 (그건 e2e).
E2E Test
실제 HTTP 요청을 보내 응답 검증. Nest 앱 실체 사용.
// test/app.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({whitelist: true, transform: true}));
await app.init();
});
afterAll(async () => {
await app.close();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
it('POST /users creates user', () => {
return request(app.getHttpServer())
.post('/users')
.send({email: 'a@b.com', password: 'secret123', name: 'kim'})
.expect(201)
.expect(res => {
expect(res.body.id).toBeDefined();
expect(res.body.email).toBe('a@b.com');
});
});
it('POST /users returns 400 on invalid input', () => {
return request(app.getHttpServer())
.post('/users')
.send({email: 'not-an-email'})
.expect(400);
});
});
Provider override
E2E 에서 특정 provider 만 mock:
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(EmailService)
.useValue({send: jest.fn().mockResolvedValue(true)})
.overrideProvider(getRepositoryToken(User))
.useValue({find: jest.fn(), save: jest.fn()})
.overrideGuard(JwtAuthGuard)
.useValue({canActivate: jest.fn().mockReturnValue(true)})
.compile();
overrideProvideroverrideGuardoverrideInterceptoroverridePipeoverrideFilter
DB 격리
패턴 1: SQLite in-memory
// test.module.ts
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [User, Order],
synchronize: true,
})
빠름, 격리 완벽. PostgreSQL-specific 기능은 불가.
패턴 2: PostgreSQL + transaction rollback
let queryRunner: QueryRunner;
beforeEach(async () => {
queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
});
afterEach(async () => {
await queryRunner.rollbackTransaction();
await queryRunner.release();
});
패턴 3: Testcontainers
import { PostgreSqlContainer } from '@testcontainers/postgresql';
let container: StartedPostgreSqlContainer;
beforeAll(async () => {
container = await new PostgreSqlContainer('postgres:16').start();
process.env.DATABASE_URL = container.getConnectionUri();
}, 60000);
afterAll(async () => {
await container.stop();
});
프로덕션 DB 와 동일 환경. Docker 필요.
Fixture / Seed
async function seedUser(prisma: PrismaService, override: Partial<User> = {}) {
return prisma.user.create({
data: {
email: `test-${Date.now()}@example.com`,
name: 'Test User',
password: 'hashed',
...override,
},
});
}
it('finds seeded user', async () => {
const user = await seedUser(prisma);
const result = await service.findOne(user.id);
expect(result.email).toBe(user.email);
});
Mocking 패턴
jest.spyOn
const spy = jest.spyOn(service, 'sendEmail').mockResolvedValue(true);
await service.notify(user);
expect(spy).toHaveBeenCalledWith(user.email);
jest.mock (모듈 전체)
jest.mock('nodemailer', () => ({
createTransport: () => ({
sendMail: jest.fn().mockResolvedValue(true),
}),
}));
MSW (외부 HTTP mock)
npm i -D msw
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
const server = setupServer(
http.get('https://api.example.com/users/:id', ({params}) => {
return HttpResponse.json({id: params.id, name: 'kim'});
}),
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Guards 테스트
Guard 자체를 직접 테스트:
describe('JwtAuthGuard', () => {
let guard: JwtAuthGuard;
let jwtService: jest.Mocked<JwtService>;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
JwtAuthGuard,
{provide: JwtService, useValue: {verifyAsync: jest.fn()}},
],
}).compile();
guard = module.get(JwtAuthGuard);
jwtService = module.get(JwtService);
});
it('returns true for valid token', async () => {
jwtService.verifyAsync.mockResolvedValue({sub: 1});
const context = createMockExecutionContext({
headers: {authorization: 'Bearer token'},
});
await expect(guard.canActivate(context)).resolves.toBe(true);
});
});
createMockExecutionContext 는 자체 헬퍼:
function createMockExecutionContext(req: Partial<Request>): ExecutionContext {
return {
switchToHttp: () => ({
getRequest: () => req,
}),
} as any;
}
Pipe 테스트
const pipe = new ValidationPipe({transform: true, whitelist: true});
const dto = {email: 'not-an-email'};
await expect(
pipe.transform(dto, {type: 'body', metatype: CreateUserDto})
).rejects.toThrow(BadRequestException);
Coverage
npm run test:cov
jest.config.js:
module.exports = {
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: '.',
testRegex: '.*\\.spec\\.ts$',
transform: {'^.+\\.(t|j)s$': 'ts-jest'},
collectCoverageFrom: ['**/*.(t|j)s'],
coverageDirectory: '../coverage',
coverageThreshold: {
global: {
statements: 80,
branches: 75,
functions: 80,
lines: 80,
},
},
};
CI 통합 예시 (GitHub Actions)
name: test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run test:cov
- run: npm run test:e2e
env:
DATABASE_URL: postgres://postgres:test@localhost:5432/postgres
함정
WARNING
E2E test 에서 useGlobalPipes/Filters/Guards 등록 안 하면 main.ts 와 다른 동작. Test 도 명시적으로 등록.
CAUTION
테스트 격리. DB, cache, external service 를 각 test 별로 reset. 아니면 flaky.
WARNING
process.env 조작. beforeEach 에서 backup, afterEach 에서 복원. 아니면 다른 test 로 leak.
IMPORTANT
Jest module reset. jest.resetModules() 로 module cache clear. Global state 있는 경우 필수.
CAUTION
Async assertion. await expect(...).rejects.toThrow() 형태. expect(...).toThrow() 는 sync.
관련 위키
- NestJS - 상위 개요
- Modules - Test 대상
- Controllers
- Providers - override
- Guards - overrideGuard
- Database - DB 격리
- FastAPI Testing - 대비
- Flask Testing - 대비
이 글의 용어 (8개)
- [FastAPI] Testingfastapi
- 정의 FastAPI Testing 은 endpoint 를 실제 서버 없이 직접 호출해 응답을 검증하는 workflow 입니다. Starlette 의 (내부적으로 sync) 또는 …
- [Flask] Testingflask
- 정의 Flask Testing 은 앱 인스턴스를 직접 감싸 HTTP 요청을 시뮬레이션하는 workflow 입니다. (Werkzeug) 로 라우팅/뷰/미들웨어까지 실제 실행하고 응…
- [Framework] NestJSframeworks
- 정의 NestJS 는 Kamil Myśliwiec 이 2017년 발표한 Node.js 서버측 프레임워크 입니다. TypeScript first, Angular 에서 영감받은 모듈…
- [NestJS] Controllersframeworks
- 정의 NestJS Controller 는 데코레이터가 붙은 클래스로, HTTP 요청을 라우팅하고 응답을 반환합니다. Method 별로 , , , , 데코레이터를 붙여 URL + …
- [NestJS] Database (TypeORM, Prisma, Mongoose, Drizzle)frameworks
- 정의 NestJS Database 통합은 여러 ORM/ODM 을 module 로 감싸 DI + 트랜잭션 + 마이그레이션을 관용화합니다. 주요 선택지: TypeORM (성숙), P…
- [NestJS] Guardsframeworks
- 정의 NestJS Guard 는 특정 요청이 controller method 에 도달하기 전에 실행되어 접근 허용 여부 를 결정합니다. Middleware 이후, Intercep…
- [NestJS] Modulesframeworks
- 정의 NestJS Module 은 데코레이터가 붙은 클래스로, 관련된 controller, provider, import/export 를 하나로 묶는 조직 단위입니다. Angul…
- [NestJS] Providers & Dependency Injectionframeworks
- 정의 NestJS Provider 는 데코레이터가 붙은 클래스 (또는 값/factory) 로, DI container 에 등록되어 다른 클래스에 주입됩니다. Service, Re…
이 개념을 다룬 위키 페이지 (9)
- wiki[Koa] Testing (Supertest, Jest, Vitest)
- wiki[Framework] NestJS
- wiki[NestJS] Config (Environment, Validation)
- wiki[NestJS] Database (TypeORM, Prisma, Mongoose, Drizzle)
- wiki[NestJS] Deployment (Docker, Kubernetes, PM2)
- wiki[NestJS] Exception Filters
- wiki[NestJS] Microservices (Kafka, NATS, RabbitMQ, gRPC, TCP)
- wiki[NestJS] Modules
- wiki[NestJS] Providers & Dependency Injection
💬 댓글