ํ๋ซํผ ๋ ๋ฆฝ์ฑ
Nest๋ ํ๋ซํผ์ ๋ ๋ฆฝ์ ์ธ ํ๋ ์์ํฌ์ ๋๋ค. ์ด๋ ๋ค์ํ ์ข ๋ฅ์ ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ๋ก์ง์ ๊ฐ๋ฐํ ์ ์์์ ์๋ฏธํฉ๋๋ค. ์๋ฅผ ๋ค์ด, ๋๋ถ๋ถ์ ์ปดํฌ๋ํธ๋ ์๋ก ๋ค๋ฅธ HTTP ์๋ฒ ํ๋ ์์ํฌ (์: Express, Fastify)๋ ๋ฌผ๋ก , ๋ค์ํ ํํ์ ์ ํ๋ฆฌ์ผ์ด์ (์: HTTP ์๋ฒ, ์๋ก ๋ค๋ฅธ ์ ์ก ๊ณ์ธต์ ์ฌ์ฉํ๋ ๋ง์ดํฌ๋ก์๋น์ค, ์น์์ผ)์์๋ ๋ณ๊ฒฝ ์์ด ์ฌ์ฌ์ฉํ ์ ์์ต๋๋ค.
ํ ๋ฒ ๋ง๋ค๊ณ ์ด๋์๋ ์ฌ์ฉํ์ธ์
๋ฌธ์์ Overview ์น์ ์์๋ ์ฃผ๋ก HTTP ์๋ฒ ํ๋ ์์ํฌ (์: REST API ์ ๊ณต ์ฑ, MVC ์คํ์ผ์ ์๋ฒ์ฌ์ด๋ ๋ ๋๋ง ์ฑ)๋ฅผ ์ฌ์ฉํ๋ ์ฝ๋ฉ ๊ธฐ๋ฒ์ ๋ณด์ฌ์ฃผ์ง๋ง, ์ด๋ฌํ ๋ชจ๋ ๊ตฌ์ฑ ์์๋ ๋ง์ดํฌ๋ก์๋น์ค๋ ์น์์ผ์ฒ๋ผ ๋ค๋ฅธ ์ ์ก ๊ณ์ธต ์์์๋ ์ฌ์ฉํ ์ ์์ต๋๋ค.
๋ํ, Nest๋ ์ ์ฉ GraphQL ๋ชจ๋์ ์ ๊ณตํ์ฌ REST API ๋์ GraphQL์ API ๊ณ์ธต์ผ๋ก ์์ ๋กญ๊ฒ ์ฌ์ฉํ ์ ์์ต๋๋ค.
๋๋ถ์ด, ์ ํ๋ฆฌ์ผ์ด์ ์ปจํ ์คํธ ๊ธฐ๋ฅ์ ํตํด CRON ์์ ์ด๋ CLI ์ฑ๊ณผ ๊ฐ์ ๋ค์ํ Node.js ์ ํ๋ฆฌ์ผ์ด์ ๋ Nest ์์์ ๋ง๋ค ์ ์์ต๋๋ค.
Nest๋ Node.js ์ ํ๋ฆฌ์ผ์ด์ ์ ์ํ ์์ ํ ํ๋ซํผ์ด ๋๋ ๊ฒ์ ๋ชฉํ๋ก ํ๋ฉฐ, ๋์ ์์ค์ ๋ชจ๋ํ์ ์ฌ์ฌ์ฉ์ฑ์ ์ ๊ณตํฉ๋๋ค. ํ ๋ฒ ๋ง๋ค๊ณ ์ด๋์๋ ์ฌ์ฉํ์ธ์!
ํ ์คํธ
์๋ํ ํ ์คํธ๋ ๋ณธ๊ฒฉ์ ์ธ ์ํํธ์จ์ด ๊ฐ๋ฐ์์ ํ์์ ์ธ ์์๋ก ๊ฐ์ฃผ๋ฉ๋๋ค. ์๋ํ๋ฅผ ํตํด ๊ฐ๋ฐ ์ค ๊ฐ๋ณ ํ ์คํธ๋ ํ ์คํธ ์ค์ํธ๋ฅผ ๋น ๋ฅด๊ณ ์ฝ๊ฒ ๋ฐ๋ณต ์คํํ ์ ์์ผ๋ฉฐ, ์ด๋ฅผ ํตํด ๋ฆด๋ฆฌ์ค๊ฐ ํ์ง ๋ฐ ์ฑ๋ฅ ๋ชฉํ๋ฅผ ์ถฉ์กฑํ๋์ง ํ์ธํ ์ ์์ต๋๋ค. ์๋ํ๋ ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง๋ฅผ ๋์ด๊ณ ๊ฐ๋ฐ์์๊ฒ ๋น ๋ฅธ ํผ๋๋ฐฑ ๋ฃจํ๋ฅผ ์ ๊ณตํด ์ค๋๋ค. ์ด๋ ๊ฐ๋ฐ์์ ์์ฐ์ฑ์ ๋์ด๋ ๋์์, ์์ค ์ฝ๋ ์ปค๋ฐ, ๊ธฐ๋ฅ ํตํฉ, ๋ฒ์ ๋ฆด๋ฆฌ์ค ๋ฑ ๊ฐ๋ฐ ์๋ช ์ฃผ๊ธฐ์ ์ค์ํ ์์ ์ ํ ์คํธ๊ฐ ์คํ๋๋๋ก ๋ณด์ฅํฉ๋๋ค.
์ด๋ฌํ ํ ์คํธ๋ ๋จ์ ํ ์คํธ, ํตํฉ ํ ์คํธ, E2E (End-to-End) ํ ์คํธ ๋ฑ ๋ค์ํ ์ ํ์ ํฌํจํฉ๋๋ค. ๊ทธ ํจ๊ณผ๋ ๋ถ๋ช ํ์ง๋ง, ์ด๊ธฐ ์ค์ ์ ๋ค์ ๋ฒ๊ฑฐ๋ก์ธ ์ ์์ต๋๋ค. Nest๋ ํจ๊ณผ์ ์ธ ํ ์คํธ๋ฅผ ํฌํจํ ๊ฐ๋ณ ๋ชจ๋ฒ ์ฌ๋ก๋ฅผ ์ฅ๋ คํ๋ฉฐ, ๊ฐ๋ฐ์์ ํ์ด ํ ์คํธ๋ฅผ ๊ตฌ์ถํ๊ณ ์๋ํํ๋ ๋ฐ ๋์์ด ๋๋ ๋ค์๊ณผ ๊ฐ์ ๊ธฐ๋ฅ๋ค์ ์ ๊ณตํฉ๋๋ค:
- ์ปดํฌ๋ํธ์ ๋ํ ๊ธฐ๋ณธ ๋จ์ ํ ์คํธ ๋ฐ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ํ e2e ํ ์คํธ๋ฅผ ์๋์ผ๋ก ์ค์บํด๋ฉ ํฉ๋๋ค.
- ๊ฒฉ๋ฆฌ๋ ๋ชจ๋ / ์ ํ๋ฆฌ์ผ์ด์ ๋ก๋๋ฅผ ๊ตฌ์ถํ ์ ์๋ ํ ์คํธ ๋ฌ๋์ ๊ฐ์ ๊ธฐ๋ณธ ๋๊ตฌ๋ค์ ์ ๊ณตํฉ๋๋ค.
- Jest ๋ฐ Supertest์์ ํตํฉ์ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํ์ง๋ง, ํน์ ํ ์คํธ ๋๊ตฌ์ ์ข ์๋์ง ์๊ณ ์ ์ฐํ๊ฒ ๋์ํฉ๋๋ค.
- Nest์ ์์กด์ฑ ์ฃผ์ ์์คํ ์ ํ ์คํธ ํ๊ฒฝ์์๋ ์ฌ์ฉํ ์ ์์ด, ์ปดํฌ๋ํธ ๋ชจํน์ด ์ฉ์ดํฉ๋๋ค.
์์ ์ธ๊ธํ๋ฏ์ด, Nest๋ ํน์ ํ ์คํธ ํ๋ ์์ํฌ๋ฅผ ๊ฐ์ ํ์ง ์์ผ๋ฏ๋ก ์ํ๋ ํ ์คํธ ๋๊ตฌ๋ฅผ ์์ ๋กญ๊ฒ ์ฌ์ฉํ ์ ์์ต๋๋ค. ํ ์คํธ ๋ฌ๋์ ๊ฐ์ ์์๋ง ๊ต์ฒดํ๋ฉด, Nest๊ฐ ์ ๊ณตํ๋ ํ ์คํธ ์ง์ ๊ธฐ๋ฅ์ ๊ทธ๋๋ก ํ์ฉํ ์ ์์ต๋๋ค.
์ค์น
์์ํ๊ธฐ ์ํด, ํ์ ํจํค์ง๋ฅผ ์ค์นํฉ๋๋ค.
$ npm i --save-dev @nestjs/testing
๋จ์ ํ ์คํธ
๋ค์ ์์ ์์๋ CatsController์ CatsService ๋ ํด๋์ค๋ฅผ ํ ์คํธํฉ๋๋ค. ์์ ์ธ๊ธํ๋ฏ์ด, Jest๋ ๊ธฐ๋ณธ ํ ์คํธ ํ๋ ์์ํฌ๋ก ์ ๊ณต๋๋ฉฐ, ํ ์คํธ ๋ฌ๋ ์ญํ ๋ฟ๋ง ์๋๋ผ ๋จ์ธ ํจ์ (assert function), ๋ชจํน ๋ฐ ์คํ์ด ๋ฑ์ ์ํ ํ ์คํธ ๋๋ธ ์ ํธ๋ฆฌํฐ๋ ์ ๊ณตํฉ๋๋ค. ์๋์ ๊ธฐ๋ณธ ํ ์คํธ์์๋ ์ด ํด๋์ค๋ค์ ์๋์ผ๋ก ์ธ์คํด์คํํ๊ณ , ์ปจํธ๋กค๋ฌ์ ์๋น์ค๊ฐ API ๊ณ์ฝ์ ์ถฉ์คํ ์ดํํ๋์ง๋ฅผ ๊ฒ์ฆํฉ๋๋ค.
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
describe('CatsController', () => {
let catsController: CatsController;
let catsService: CatsService;
beforeEach(() => {
catsService = new CatsService();
catsController = new CatsController(catsService);
});
describe('findAll', () => {
it('should return an array of cats', async () => {
const result = ['test'];
jest.spyOn(catsService, 'findAll').mockImplementation(() => result);
expect(await catsController.findAll()).toBe(result);
});
});
});
ํํธ
ํ ์คํธ ํ์ผ์ ํ ์คํธ ๋์ ํด๋์ค ๊ทผ์ฒ์ ์์น์ํค๋ ๊ฒ์ด ์ข์ต๋๋ค. ํ ์คํธ ํ์ผ์ ์ด๋ฆ์ .spec ๋๋ .test ์ ๋ฏธ์ฌ๋ฅผ ํฌํจํด์ผ ํฉ๋๋ค.
์์ ์์ ๋ ๋จ์ํ ํํ์ด๊ธฐ ๋๋ฌธ์ Nest ๊ณ ์ ์ ๊ธฐ๋ฅ์ ์ค์ ๋ก ํ ์คํธํ๊ณ ์๋ ๊ฒ์ ์๋๋๋ค. ์ค์ ๋ก ์์กด์ฑ ์ฃผ์ ๋ ์ฌ์ฉํ์ง ์์์ต๋๋ค (์: catsController์ CatsService ์ธ์คํด์ค๋ฅผ ์ง์ ์ ๋ฌํจ). ์ด์ฒ๋ผ ํ ์คํธ ๋์ ํด๋์ค๋ฅผ ์๋์ผ๋ก ์ธ์คํด์คํํ์ฌ ํ๋ ์์ํฌ์ ๋ฌด๊ดํ๊ฒ ์ํํ๋ ํ ์คํธ๋ฅผ ์ผ๋ฐ์ ์ผ๋ก '๊ฒฉ๋ฆฌ ํ ์คํธ (isolated testing)'๋ผ๊ณ ๋ถ๋ฆ ๋๋ค.
์ด์ Nest์ ๋ค์ํ ๊ธฐ๋ฅ์ ์ ๊ทน ํ์ฉํ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ํ ์คํธํ ์ ์๋๋ก ๋์์ฃผ๋ ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
ํ ์คํธ ์ ํธ๋ฆฌํฐ
@nestjs/testing ํจํค์ง๋ ๋ณด๋ค ๊ฒฌ๊ณ ํ ํ ์คํธ๋ฅผ ๊ฐ๋ฅํ๊ฒ ํด ์ฃผ๋ ์ ํธ๋ฆฌํฐ๋ค์ ์ ๊ณตํฉ๋๋ค. ์์ ์์ ๋ฅผ Nest์์ ์ ๊ณตํ๋ Test ํด๋์ค๋ฅผ ์ฌ์ฉํ์ฌ ๋ค์ ์์ฑํด ๋ณด๊ฒ ์ต๋๋ค.
import { Test } from '@nestjs/testing';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
describe('CatsController', () => {
let catsController: CatsController;
let catsService: CatsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();
catsService = moduleRef.get(CatsService);
catsController = moduleRef.get(CatsController);
});
describe('findAll', () => {
it('should return an array of cats', async () => {
const result = ['test'];
jest.spyOn(catsService, 'findAll').mockImplementation(() => result);
expect(await catsController.findAll()).toBe(result);
});
});
});
Test ํด๋์ค๋ Nest์ ์ ์ฒด ๋ฐํ์์ ๋ชจ์ (mock) ํ๋ ์คํ ์ปจํ ์คํธ๋ฅผ ์ ๊ณตํ๋ฉฐ, ํด๋์ค ์ธ์คํด์ค๋ฅผ ๊ด๋ฆฌํ๊ฑฐ๋ ๋ชจํน/์ค๋ฒ๋ผ์ด๋ฉํ๊ธฐ ์ฝ๊ฒ ๋์์ฃผ๋ ํ ๋ค์ ์ ๊ณตํฉ๋๋ค. ์ด ํด๋์ค์ createTestingModule() ๋ฉ์๋๋ @Module() ๋ฐ์ฝ๋ ์ดํฐ์ ์ ๋ฌํ๋ ๊ฒ๊ณผ ๋์ผํ ๋ชจ๋ ๋ฉํ๋ฐ์ดํฐ ๊ฐ์ฒด๋ฅผ ์ธ์๋ก ๋ฐ์ต๋๋ค. ์ด ๋ฉ์๋๋ TestingModule ์ธ์คํด์ค๋ฅผ ๋ฐํํ๋ฉฐ, ์ด ์ธ์คํด์ค๋ ๋ช ๊ฐ์ง ์ ์ฉํ ๋ฉ์๋๋ฅผ ์ ๊ณตํฉ๋๋ค.
๋จ์ ํ ์คํธ์์ ์ค์ํ ๋ฉ์๋๋ compile()์ ๋๋ค. ์ด ๋ฉ์๋๋ ์์กด์ฑ์ ํฌํจํ ๋ชจ๋์ ๋ถํธ์คํธ๋ฉํ๋ฉฐ (์ผ๋ฐ์ ์ธ main.ts ์์ NestFactory.create()๋ก ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ถํธ์คํธ๋ฉํ๋ ๋ฐฉ์๊ณผ ์ ์ฌํจ), ํ ์คํธํ ์ ์๋ ์ํ์ ๋ชจ๋์ ๋ฐํํฉ๋๋ค.
ํํธ
compile() ๋ฉ์๋๋ ๋น๋๊ธฐ ํจ์์ด๋ฏ๋ก await ํค์๋๋ฅผ ์ฌ์ฉํด ํธ์ถํด์ผ ํฉ๋๋ค. ๋ชจ๋์ด ์ปดํ์ผ๋ ํ์๋, get() ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ํด๋น ๋ชจ๋์ ์ ์๋ ์ปจํธ๋กค๋ฌ๋ ํ๋ก๋ฐ์ด๋์ ๊ฐ์ ์ ์ ์ธ์คํด์ค๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค.
TestingModule์ ModuleRef ํด๋์ค๋ฅผ ์์๋ฐ๊ธฐ ๋๋ฌธ์, transient ๋๋ request-scoped์ ๊ฐ์ ์ค์ฝํ๊ฐ ์ง์ ๋ ํ๋ก๋ฐ์ด๋๋ฅผ ๋์ ์ผ๋ก resolve ํ ์ ์๋ ๊ธฐ๋ฅ์ ๊ฐ์ง๋๋ค. ์ด๋ฌํ ๋์ ์ธ์คํด์ค๋ฅผ ๊ฐ์ ธ์ฌ ๋๋ get() ์ด ์๋ resolve() ๋ฉ์๋๋ฅผ ์ฌ์ฉํด์ผ ํฉ๋๋ค. get() ๋ฉ์๋๋ ์ ์ ์ธ์คํด์ค๋ง ๊ฐ์ ธ์ฌ ์ ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();
catsService = await moduleRef.resolve(CatsService);
์ฃผ์
resolve() ๋ฉ์๋๋ ํด๋น ํ๋ก๋ฐ์ด๋์ ๊ณ ์ ํ DI ์ปจํ ์ด๋ ์๋ธ ํธ๋ฆฌ๋ก๋ถํฐ ์๋ก์ด ์ธ์คํด์ค๋ฅผ ๋ฐํํฉ๋๋ค. ๊ฐ ์๋ธ ํธ๋ฆฌ๋ ๊ณ ์ ํ ์ปจํ ์คํธ ์๋ณ์ (context identifier)๋ฅผ ๊ฐ์ง๋ฏ๋ก, ์ด ๋ฉ์๋๋ฅผ ์ฌ๋ฌ ๋ฒ ํธ์ถํ๋ฉด ๋งค๋ฒ ์๋ก ๋ค๋ฅธ ์ธ์คํด์ค๋ฅผ ์ป๊ฒ ๋ฉ๋๋ค. ๋ฐ๋ผ์ ๋ฐํ๋ ์ธ์คํด์ค๋ฅผ ๋น๊ตํ๋ฉด ์ฐธ์กฐ๊ฐ์ด ์๋ก ๋ค๋ฆ์ ํ์ธํ ์ ์์ต๋๋ค.
ํํธ
๋ชจ๋ ์ฐธ์กฐ ๊ธฐ๋ฅ์ ๋ํด ๋ ์๊ณ ์ถ๋ค๋ฉด ์ฌ๊ธฐ์์ ํ์ธํด ๋ณด์ธ์.
์ค์ ํ๋ก๋ฐ์ด๋ (Production ์ฉ)๋ฅผ ์ฌ์ฉํ๋ ๋์ , ํ ์คํธ ๋ชฉ์ ์ ๋ง๊ฒ ์ปค์คํ ํ๋ก๋ฐ์ด๋๋ก ์ค๋ฒ๋ผ์ด๋ํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, ์ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ฐ๊ฒฐํ๋ ๋์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์๋น์ค๋ฅผ ๋ชจํน ํ์ฌ ํ ์คํธํ ์ ์์ต๋๋ค. ์ด๋ฌํ ์ค๋ฒ๋ผ์ด๋ ๊ธฐ๋ฅ์ ๋ค์ ์น์ ์์ ์์ธํ ๋ค๋ฃฐ ์์ ์ด์ง๋ง, ๋จ์ ํ ์คํธ์์๋ ์ฌ์ฉํ ์ ์์ต๋๋ค.
์๋ ๋ชจํน
Nest๋ ๋๋ฝ๋ ์์กด์ฑ ์ ์ฒด์ ๊ณตํต์ ์ผ๋ก ์ ์ฉํ ์ ์๋ ๋ชจ์ (mock) ํฉํ ๋ฆฌ๋ฅผ ์ ์ํ ์ ์๋๋ก ์ง์ํฉ๋๋ค. ์ด ๊ธฐ๋ฅ์ ํ ์คํธ ๋์ ํด๋์ค์ ๋ง์ ์์กด์ฑ์ด ์์ ๋, ์ผ์ผ์ด ๋ชจํน ํ๋ ๋ฐ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฌ๋ ๊ฒฝ์ฐ์ ์ ์ฉํฉ๋๋ค. ์ด๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด createTestingModule() ์ดํ์ useChecker() ๋ฉ์๋๋ฅผ ์ฒด์ด๋ ํ์ฌ, ์์กด์ฑ ๋ชจํน์ ์ํ ํฉํ ๋ฆฌ๋ฅผ ์ ๋ฌํด์ผ ํฉ๋๋ค. ์ด ํฉํ ๋ฆฌ ํจ์๋ ์ ํ์ ์ผ๋ก ํ ํฐ (token)์ ์ธ์๋ก ๋ฐ์ ์ ์์ผ๋ฉฐ, ์ด ํ ํฐ์ Nest ํ๋ก๋ฐ์ด๋๋ก ์ ํจํ ์ด๋ค ํ ํฐ์ด๋ ๊ฐ๋ฅํฉ๋๋ค. ํฉํ ๋ฆฌ ํจ์๋ ํด๋น ํ ํฐ์ ๋ํ ๋ชจ์ ๊ตฌํ์ ๋ฐํํฉ๋๋ค. ์๋๋ jest-mock์ ์ฌ์ฉํ์ฌ ์ผ๋ฐ์ ์ธ mocker๋ฅผ ์์ฑํ๊ณ , CatsService์ ๋ํด์๋ jest.fn()์ ํ์ฉํ ํน์ ๋ชจํน์ ์ ์ฉํ ์์์ ๋๋ค.
// ...
import { ModuleMocker, MockMetadata } from 'jest-mock';
const moduleMocker = new ModuleMocker(global);
describe('CatsController', () => {
let controller: CatsController;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
})
.useMocker((token) => {
const results = ['test1', 'test2'];
if (token === CatsService) {
return { findAll: jest.fn().mockResolvedValue(results) };
}
if (typeof token === 'function') {
const mockMetadata = moduleMocker.getMetadata(
token,
) as MockMetadata<any, any>;
const Mock = moduleMocker.generateFromMetadata(
mockMetadata,
) as ObjectConstructor;
return new Mock();
}
})
.compile();
controller = moduleRef.get(CatsController);
});
});
์ด๋ฌํ ๋ชจํน ๋ ์ธ์คํด์ค๋ค๋ ์ผ๋ฐ ์ปค์คํ ํ๋ก๋ฐ์ด๋์ ๋ง์ฐฌ๊ฐ์ง๋ก ํ ์คํธ ์ปจํ ์ด๋์์ ๊บผ๋ด์ฌ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, moduleRef.get(CatsService)์ ๊ฐ์ด ํธ์ถํ๋ฉด ํด๋น ๋ชจํน๋ CatsService ์ธ์คํด์ค๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค.
ํํธ
@golevelup/ts-jest์ createMock๊ณผ ๊ฐ์ ์ผ๋ฐ์ ์ธ ๋ชจ์ (mock) ํฉํ ๋ฆฌ๋ useMocker()์ ์ง์ ์ ๋ฌํ์ฌ ์ฌ์ฉํ ์ ์์ต๋๋ค.
ํํธ
REQUEST์ INQUIRER ํ๋ก๋ฐ์ด๋๋ Nest ์ปจํ ์คํธ ๋ด์ ์ด๋ฏธ ์ฌ์ ์ ์๋์ด ์๊ธฐ ๋๋ฌธ์ ์๋ ๋ชจํน์ด ๋ถ๊ฐ๋ฅํฉ๋๋ค. ํ์ง๋ง, ์ปค์คํ ํ๋ก๋ฐ์ด๋ ๋ฌธ๋ฒ์ ์ฌ์ฉํ๊ฑฐ๋ .overrideProvider ๋ฉ์๋๋ฅผ ํ์ฉํ์ฌ ์ค๋ฒ๋ผ์ด๋ํ ์ ์์ต๋๋ค.
End to End ํ ์คํธ
๋จ์ ํ ์คํธ๊ฐ ๊ฐ๋ณ ๋ชจ๋์ด๋ ํด๋์ค์ ์ด์ ์ ๋ง์ถ ๋ฐ๋ฉด, E2E (End-to-End) ํ ์คํธ๋ ํด๋์ค์ ๋ชจ๋ ๊ฐ์ ์ํธ์์ฉ์ ๋ ํฐ ์์ค์์ ๊ฒ์ฆํ๋ฉฐ, ์ด๋ ์ค์ ์ฌ์ฉ์๊ฐ ํ๋ก๋์ ์์คํ ๊ณผ ์ํธ์์ฉํ๋ ๋ฐฉ์์ ๋ ๊ฐ๊น์ต๋๋ค. ์ ํ๋ฆฌ์ผ์ด์ ์ด ์ปค์ง์๋ก ๊ฐ API ์๋ํฌ์ธํธ์ E2E ๋์์ ์๋์ผ๋ก ํ ์คํธํ๋ ๊ฒ์ ์ด๋ ค์์ง๋๋ค. ์๋ํ๋ E2E ํ ์คํธ๋ ์์คํ ์ ์ ์ฒด์ ์ธ ๋์์ด ์ฌ๋ฐ๋ฅด๋ฉฐ ํ๋ก์ ํธ ์๊ตฌ์ฌํญ์ ์ถฉ์กฑํ๋์ง๋ฅผ ํ์ธํ๋ ๋ฐ ๋์์ด ๋ฉ๋๋ค. E2E ํ ์คํธ๋ฅผ ์ํํ ๋๋ ์์ ์ดํด๋ณธ ๋จ์ ํ ์คํธ ๊ตฌ์ฑ๊ณผ ์ ์ฌํ ๋ฐฉ์์ ์ฌ์ฉํฉ๋๋ค. ๋ํ, Nest๋ Supertest ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํด HTTP ์์ฒญ์ ์๋ฎฌ๋ ์ด์ ํ๋ ์์ ๋ ์ฝ๊ฒ ํ ์ ์๋๋ก ์ง์ํฉ๋๋ค.
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { CatsModule } from '../../src/cats/cats.module';
import { CatsService } from '../../src/cats/cats.service';
import { INestApplication } from '@nestjs/common';
describe('Cats', () => {
let app: INestApplication;
let catsService = { findAll: () => ['test'] };
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [CatsModule],
})
.overrideProvider(CatsService)
.useValue(catsService)
.compile();
app = moduleRef.createNestApplication();
await app.init();
});
it(`/GET cats`, () => {
return request(app.getHttpServer())
.get('/cats')
.expect(200)
.expect({
data: catsService.findAll(),
});
});
afterAll(async () => {
await app.close();
});
});
ํํธ
Fastify๋ฅผ HTTP ์ด๋ํฐ๋ก ์ฌ์ฉํ๋ ๊ฒฝ์ฐ, ์ฝ๊ฐ ๋ค๋ฅธ ์ค์ ์ด ํ์ํ๋ฉฐ ์์ฒด์ ์ธ ํ ์คํธ ๊ธฐ๋ฅ๋ ๋ด์ฅ๋์ด ์์ต๋๋ค.
let app: NestFastifyApplication;
beforeAll(async () => {
app = moduleRef.createNestApplication<NestFastifyApplication>(
new FastifyAdapter(),
);
await app.init();
await app.getHttpAdapter().getInstance().ready();
});
it(`/GET cats`, () => {
return app
.inject({
method: 'GET',
url: '/cats',
})
.then((result) => {
expect(result.statusCode).toEqual(200);
expect(result.payload).toEqual(/* expectedPayload */);
});
});
afterAll(async () => {
await app.close();
});โ
์ด๋ฒ ์์ ์์๋ ์์์ ์ค๋ช ํ ๊ฐ๋ ๋ค์ ํ์ฅํ์ฌ ์ฌ์ฉํฉ๋๋ค. ์ด์ ์ ์ฌ์ฉํ๋ compile() ๋ฉ์๋์ ๋ํด, ์ด๋ฒ์๋ createNestApplication() ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ์ ์ฒด Nest ๋ฐํ์ ํ๊ฒฝ์ ์ธ์คํด์คํํฉ๋๋ค.
์ฃผ์ํ ์ ์, compile() ๋ฉ์๋๋ก ์ ํ๋ฆฌ์ผ์ด์ ์ ์ปดํ์ผํ ๋๋ ์์ง HTTP ์ด๋ํฐ๋ ์๋ฒ๊ฐ ์์ฑ๋์ง ์์๊ธฐ ๋๋ฌธ์ HttpAdapterHost#httpAdapter ๊ฐ์ด undefined ์ํ๋ผ๋ ๊ฒ์ ๋๋ค. ๋ฐ๋ผ์ ํ ์คํธ ์ค httpAdapter๊ฐ ํ์ํ๋ค๋ฉด createNestApplication()์ ์ฌ์ฉํ์ฌ ์ ํ๋ฆฌ์ผ์ด์ ์ธ์คํด์ค๋ฅผ ์์ฑํด์ผ ํฉ๋๋ค. ๋๋ ์์กด์ฑ ๊ทธ๋ํ๋ฅผ ์ด๊ธฐํํ ๋ ํด๋น ์์กด์ฑ์ด ํ์ํ์ง ์๋๋ก ํ๋ก์ ํธ ๊ตฌ์กฐ๋ฅผ ๋ฆฌํฉํฐ๋ง ํ๋ ๋ฐฉ๋ฒ๋ ๊ณ ๋ คํด์ผ ํฉ๋๋ค.
์ข์ต๋๋ค. ์์ ๋ฅผ ํ๋์ฉ ์ดํด๋ด ์๋ค:
์ฐ๋ฆฌ๋ ์คํ ์ค์ธ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ํ ์ฐธ์กฐ๋ฅผ app ๋ณ์์ ์ ์ฅํฉ๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด HTTP ์์ฒญ์ ์๋ฎฌ๋ ์ด์ ํ ์ ์์ต๋๋ค.
Supertest์ request() ํจ์๋ฅผ ์ฌ์ฉํ์ฌ HTTP ํ ์คํธ๋ฅผ ์๋ฎฌ๋ ์ด์ ํฉ๋๋ค. ์ด๋ฌํ HTTP ์์ฒญ์ด ์คํ ์ค์ธ Nest ์ฑ์ผ๋ก ๋ผ์ฐํ ๋๋๋ก ํ๋ ค๋ฉด, Nest์ ๊ธฐ๋ฐ์ด ๋๋ HTTP ๋ฆฌ์ค๋ (Express ํ๋ซํผ์์ ์ ๊ณต๋ ์ ์์)์ ๋ํ ์ฐธ์กฐ๋ฅผ request()์ ์ ๋ฌํฉ๋๋ค. ๋ฐ๋ผ์ request(app.getHttpServer())๋ผ๋ ๊ตฌ๋ฌธ์ด ์ฌ์ฉ๋ฉ๋๋ค. request() ํธ์ถ์ Nest ์ฑ์ ์ฐ๊ฒฐ๋ HTTP ์๋ฒ๋ฅผ ๊ฐ์ผ ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ฉฐ, ์ด๋ฅผ ํตํด ์ค์ HTTP ์์ฒญ์ ์๋ฎฌ๋ ์ด์ ํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, request(...).get('/cats')๋ผ๋ Nest ์ฑ์ผ๋ก ๋ค์ด์ค๋ ์ค์ GET /cats ์์ฒญ๊ณผ ๋์ผํ ๋์์ ์ํํฉ๋๋ค.
์ด ์์ ์์๋ CatsService์ ๋์ฒด (ํ ์คํธ์ฉ) ๊ตฌํ๋ ์ ๊ณตํฉ๋๋ค. ์ด๋ ํ๋์ฝ๋ฉ๋ ๊ฐ์ ๋ฐํํ๋๋ก ์ค์ ๋๋ฉฐ, ํ ์คํธ์์ ๊ทธ ๊ฐ์ ๊ฒ์ฆํ ์ ์์ต๋๋ค. ์ด๋ฐ ๋์ฒด ๊ตฌํ์ ์ ๊ณตํ๋ ค๋ฉด overrideProvider()๋ฅผ ์ฌ์ฉํฉ๋๋ค. Nest๋ ์ด์ ์ ์ฌํ๊ฒ overrideModule(), overrideGuard(), overrideInterceptor(), overrideFilter(), overridePipe() ๋ฉ์๋๋ฅผ ํตํด ๋ชจ๋, ๊ฐ๋, ์ธํฐ์ ํฐ, ํํฐ, ํ์ดํ๋ ์ค๋ฒ๋ผ์ด๋ํ ์ ์๋๋ก ์ง์ํฉ๋๋ค.
์ด๋ฌํ override ๋ฉ์๋๋ค (๋จ, overrideModule() ์ ์ธ)์ ์ฌ์ฉ์ ์ ์ ๊ณต๊ธ์ ๋ฉ์๋์ ๋ํด ์ค๋ช ๋ ๋ฉ์๋๋ฅผ ๋ฐ์ํ๋ 3๊ฐ์ง ๋ค๋ฅธ ๋ฉ์๋๊ฐ ์๋ ๊ฐ์ฒด๋ฅผ ๋ฐํํฉ๋๋ค.
- useClass: ์ง์ ํ ํด๋์ค๋ฅผ ์ธ์คํด์คํํ์ฌ ์ค๋ธ์ ํธ (ํ๋ก๋ฐ์ด๋, ๊ฐ๋ ๋ฑ)๋ฅผ ๋์ฒดํฉ๋๋ค.
- useValue: ์ง์ ํ ์ธ์คํด์ค๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ์ฌ ์ค๋ธ์ ํธ๋ฅผ ๋์ฒดํฉ๋๋ค.
- useFactory: ์ธ์คํด์ค๋ฅผ ๋ฐํํ๋ ํจ์๋ฅผ ์ ๊ณตํ์ฌ ์ค๋ธ์ ํธ๋ฅผ ๋์ฒดํฉ๋๋ค.
๋ฐ๋ฉด, overrideModule()์ useModule() ๋ฉ์๋๋ฅผ ๋ฐํํ๋ฉฐ, ์ด๋ฅผ ํตํด ๊ธฐ์กด ๋ชจ๋์ ๋ค์๊ณผ ๊ฐ์ด ์๋ก์ด ๋ชจ๋๋ก ๋์ฒดํ ์ ์์ต๋๋ค.
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
})
.overrideModule(CatsModule)
.useModule(AlternateCatsModule)
.compile();
๊ฐ override ๋ฉ์๋๋ ๊ฒฐ๊ตญ TestingModule ์ธ์คํด์ค๋ฅผ ๋ฐํํ๋ฏ๋ก, ๋ค๋ฅธ ๋ฉ์๋๋ค๊ณผ ์ฒด์ด๋ (fluent style)์ผ๋ก ์ฐ๊ฒฐํ ์ ์์ต๋๋ค. ์ด๋ฐ ์ฒด์ธ์ ์ฌ์ฉํ ๋๋ ๋ง์ง๋ง์ compile()์ ํธ์ถํ์ฌ Nest๊ฐ ๋ชจ๋์ ์ธ์คํด์คํํ๊ณ ์ด๊ธฐํํ๋๋ก ํด์ผ ํฉ๋๋ค.
๋ํ, ํ ์คํธ ์คํ ์ (์: CI ์๋ฒ์์) ์ปค์คํ ๋ก๊ฑฐ๋ฅผ ์ฌ์ฉํ๊ณ ์ถ์ ๋๋ ์์ต๋๋ค. ์ด๋ด ๋๋ setLogger() ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๊ณ , LoggerService ์ธํฐํ์ด์ค๋ฅผ ๋ง์กฑํ๋ ๊ฐ์ฒด๋ฅผ ์ ๋ฌํ์ฌ ํ ์คํธ ์ค ๋ก๊น ๋ฐฉ์์ ์ง์ ํ ์ ์์ต๋๋ค. (๊ธฐ๋ณธ์ ์ผ๋ก๋ "error" ๋ ๋ฒจ์ ๋ก๊ทธ๋ง ์ฝ์์ ์ถ๋ ฅ๋ฉ๋๋ค.)
์ปดํ์ผ๋ ๋ชจ๋์ ๋ค์ ํ์ ์ค๋ช ๋ ์ฌ๋ฌ ์ ์ฉํ ๋ฉ์๋๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค:
createNestApplication() | ์ฃผ์ด์ง ๋ชจ๋์ ๊ธฐ๋ฐ์ผ๋ก Nest ์ ํ๋ฆฌ์ผ์ด์ (INestApplication ์ธ์คํด์ค)์ ์์ฑํ๊ณ ๋ฐํํฉ๋๋ค. init() ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ์ ํ๋ฆฌ์ผ์ด์ ์ ์๋์ผ๋ก ์ด๊ธฐํํด์ผ ํ๋ค๋ ์ ์ ์ ์ํ์ธ์. |
createNestMicroservice() | ์ฃผ์ด์ง ๋ชจ๋์ ๊ธฐ๋ฐ์ผ๋ก Nest ๋ง์ดํฌ๋ก์๋น์ค (INestMicroservice ์ธ์คํด์ค)๋ฅผ ์์ฑํ๊ณ ๋ฐํํฉ๋๋ค. |
get() | ์ ํ๋ฆฌ์ผ์ด์ ์ปจํ ์คํธ์์ ์ฌ์ฉ ๊ฐ๋ฅํ ์ปจํธ๋กค๋ฌ ๋๋ ํ๋ก๋ฐ์ด๋ (๊ฐ๋, ํํฐ ๋ฑ ํฌํจ)์ ์ ์ ์ธ์คํด์ค๋ฅผ ๊ฒ์ํฉ๋๋ค. ๋ชจ๋ ์ฐธ์กฐ ํด๋์ค์์ ์์๋ฉ๋๋ค. |
resolve() | ์ ํ๋ฆฌ์ผ์ด์ ์ปจํ ์คํธ์์ ์ฌ์ฉ ๊ฐ๋ฅํ ์ปจํธ๋กค๋ฌ ๋๋ ํ๋ก๋ฐ์ด๋ (๊ฐ๋, ํํฐ ๋ฑ ํฌํจ)์ ๋์ ์ผ๋ก ์์ฑ๋ ์ค์ฝํ ์ธ์คํด์ค (request ๋๋ transient)๋ฅผ ๊ฒ์ํฉ๋๋ค. ๋ชจ๋ ์ฐธ์กฐ ํด๋์ค์์ ์์๋ฉ๋๋ค. |
select() | ๋ชจ๋์ ์์กด์ฑ ๊ทธ๋ํ๋ฅผ ํ์ํฉ๋๋ค. ํน์ ๋ชจ๋์์ ์ธ์คํด์ค๋ฅผ ๊ฒ์ํ ๋ ์ฌ์ฉ๋๋ฉฐ, get() ๋ฉ์๋์ strict ๋ชจ๋ (strict: true)์ ํจ๊ป ์ฌ์ฉ๋ฉ๋๋ค. |
ํํธ
E2E ํ ์คํธ ํ์ผ์ test ๋๋ ํฐ๋ฆฌ ์์ ๋์ด์ผ ํฉ๋๋ค. ํ ์คํธ ํ์ผ์ ์ด๋ฆ์ .e2e-spec ์ ๋ฏธ์ฌ๋ฅผ ๊ฐ์ ธ์ผ ํฉ๋๋ค.
์ ์ญ์ผ๋ก ๋ฑ๋ก๋ ํฅ์์ (Enhancer) ์ฌ์ ์ํ๊ธฐ
์ ์ญ์ผ๋ก ๋ฑ๋ก๋ ๊ฐ๋ (๋๋ ํ์ดํ, ์ธํฐ์ ํฐ, ํํฐ)๋ฅผ ์ฌ์ ์ํ๋ ค๋ฉด ๋ช ๊ฐ์ง ์ถ๊ฐ ๋จ๊ณ๋ฅผ ์ํํด์ผ ํฉ๋๋ค. ์๋์ ๋ฑ๋ก ๋ฐฉ์์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
์ด ์ฝ๋๋ APP_* ํ ํฐ์ ํตํด ๊ฐ๋๋ฅผ "๋ฉํฐ" ํ๋ก๋ฐ์ด๋๋ก ๋ฑ๋กํ๋ ๊ฒ์ ๋๋ค. ์ฌ๊ธฐ์์ JwtAuthGuard๋ฅผ ๊ต์ฒดํ๋ ค๋ฉด, ํด๋น ์์น์ ๊ธฐ์กด ํ๋ก๋ฐ์ด๋๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ์์ผ๋ก ๋ฑ๋กํด์ผ ํฉ๋๋ค:
providers: [
{
provide: APP_GUARD,
useExisting: JwtAuthGuard,
// ^^^^^^^^ 'useClass' ๋์ 'useExisting'์ ์ฌ์ฉํ ์ ์ ์ฃผ๋ชฉํ์ธ์
},
JwtAuthGuard,
],
ํํธ
Nest๊ฐ ํ ํฐ ๋ค์์ ์ธ์คํด์ค๋ฅผ ์์ฑํ์ง ์๋๋ก ํ๋ ค๋ฉด useClass ๋์ useExisting์ ์ฌ์ฉํ์ฌ ์ด๋ฏธ ๋ฑ๋ก๋ ํ๋ก๋ฐ์ด๋๋ฅผ ์ฐธ์กฐํ์ธ์.
์ด์ JwtAuthGuard๋ Nest์ ์ผ๋ฐ ํ๋ก๋ฐ์ด๋๋ก ์ธ์๋๋ฏ๋ก, TestingModule์ ์์ฑํ ๋ ์ฌ์ ์ํ ์ ์์ต๋๋ค:
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(JwtAuthGuard)
.useClass(MockAuthGuard)
.compile();
์ด์ ๋ชจ๋ ํ ์คํธ์์ ๋ชจ๋ ์์ฒญ์ ๋ํด MockAuthGuard๊ฐ ์ฌ์ฉ๋ฉ๋๋ค.
์์ฒญ ๋ฒ์ ์ธ์คํด์ค ํ ์คํธํ๊ธฐ
์์ฒญ ๋ฒ์ ํ๋ก๋ฐ์ด๋๋ ๊ฐ ๋ค์ด์ค๋ ์์ฒญ๋ง๋ค ๊ณ ์ ํ๊ฒ ์์ฑ๋ฉ๋๋ค. ํด๋น ์ธ์คํด์ค๋ ์์ฒญ ์ฒ๋ฆฌ๊ฐ ์๋ฃ๋๋ฉด ๊ฐ๋น์ง ์ปฌ๋ ์ ๋ฉ๋๋ค. ์ด๋ก ์ธํด ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋๋ฐ, ํ ์คํธ๋ ์์ฒญ์ ์ํด ํน๋ณํ ์์ฑ๋ ์์กด์ฑ ์ฃผ์ ํ์ ํธ๋ฆฌ์ ์ ๊ทผํ ์ ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
์์์ ์ค๋ช ํ ๋ด์ฉ์ ๋ฐํ์ผ๋ก, resolve() ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ๋์ ์ผ๋ก ์ธ์คํด์คํ๋ ํด๋์ค๋ฅผ ๊ฒ์ํ ์ ์๋ค๋ ๊ฒ์ ์๊ณ ์์ต๋๋ค. ๋ํ ์ฌ๊ธฐ์์ ์ค๋ช ํ ๊ฒ์ฒ๋ผ ๊ณ ์ ํ ์ปจํ ์คํธ ์๋ณ์๋ฅผ ์ ๋ฌํ์ฌ DI ์ปจํ ์ด๋ ํ์ ํธ๋ฆฌ์ ์๋ช ์ฃผ๊ธฐ๋ฅผ ์ ์ดํ ์ ์์ต๋๋ค. ๊ทธ๋ ๋ค๋ฉด ํ ์คํธ ํ๊ฒฝ์์๋ ์ด๋ฅผ ์ด๋ป๊ฒ ํ์ฉํ ์ ์์๊น์?
์ ๋ต์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค. ์ฌ์ ์ ์ปจํ ์คํธ ์๋ณ์๋ฅผ ์์ฑํ ํ, Nest๊ฐ ๋ชจ๋ ๋ค์ด์ค๋ ์์ฒญ์ ๋ํด ์ด ์๋ณ์๋ฅผ ์ฌ์ฉํ๋๋ก ๊ฐ์ ํ๋ ๊ฒ์ ๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ํ ์คํธ๋ ์์ฒญ์ ๋ํด ์์ฑ๋ ์ธ์คํด์ค๋ฅผ ๊ฒ์ํ ์ ์๊ฒ ๋ฉ๋๋ค.
์ด๋ฅผ ์ํด ContextIdFactory์ ๋ํด jest.spyOn()์ ์ฌ์ฉํฉ๋๋ค.
const contextId = ContextIdFactory.create();
jest
.spyOn(ContextIdFactory, 'getByRequest')
.mockImplementation(() => contextId);
์ด์ contextId๋ฅผ ์ฌ์ฉํ์ฌ ์ดํ์ ๋ชจ๋ ์์ฒญ์ ๋ํด ์์ฑ๋ DI ์ปจํ ์ด๋ ํ์ ํธ๋ฆฌ ํ๋์ ์ ๊ทผํ ์ ์์ต๋๋ค.
catsService = await moduleRef.resolve(CatsService, contextId);
Reference
'๐ ๊ณต์ ๋ฌธ์ ๋ฒ์ญ > Nest.js' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Nest.js] Fundamentals | Lifecycle events, Discovery service (1) | 2025.07.29 |
---|---|
[Nest.js] Fundamentals | Lazy-loading modules, Execution context (3) | 2025.07.29 |
[Nest.js] Fundamentals - Circular dependency, Module reference (0) | 2025.07.19 |
[Nest.js] Fundamentals - Dynamic modules, Injection scopes (0) | 2025.07.16 |
[Nest.js] Fundamentals - Custom providers, Asynchronous providers (0) | 2025.07.15 |