๋จ์ ํ ์คํธ๋ฅผ ๋ ๊ฐ๋จํ๊ฒ ์์ฑํ ์ ์๋ ๋ฐฉ๋ฒ์ด ์์๊น...? ๐ค
๋จ์ ํ
์คํธ(Unit Test)๋ฅผ ์์ฑํ๋ค๋ณด๋ฉด ๊ฐ์ฅ ๋ต๋ตํ ๋ถ๋ถ์ ์ญ์ ์์กด์ฑ๋ค์ ๋ชจ์(mocking)ํ๋ ๊ฒ์
๋๋ค.
๋ฐฑ์๋์์ ๋จ์ ํ
์คํธ๋ฅผ ํ๋ฉด ์ข์๋งํ ๊ฒ์ ์ญ์ ์๋น์ค ๋ ์ด์ด์ผ ๊ฒ์
๋๋ค.
์ ๋ NestJS์์ ์ธ๋ถ API๋ฅผ ํธ์ถํ์ฌ ์๋ฆผ์ ์ ์กํ๋ ์๋น์ค(NotificationService)๋ฅผ ๋ง๋ค์์ต๋๋ค.
์ด ์๋น์ค์ ํจ์๋ค์ ํ
์คํธํ๋ ค๋ฉด ์ธ๋ถ API๋ฅผ ์ค์ ๋ก ์ฐ๊ฒฐํ์ง ์๊ณ , ๋ง์น ์ฐ๊ฒฐํ ๊ฒ์ฒ๋ผ ๋ชจํนํ๋ ๊ณผ์ ์ด ํ์ํ์ต๋๋ค.
๋น์ฆ๋์ค ๋ก์ง & ํ ์คํธ ์ฝ๋
1. ๋น์ฆ๋์ค ๋ก์ง
NotificationService๋ ConfigService, HttpService, LoggerService ์ธ ๊ฐ์ ์์กด์ฑ์ ๊ฐ์ง๊ณ ์์ต๋๋ค.
๋์ค์ฝ๋ ์นํ ์ ์ฌ์ฉํด ๋์ค์ฝ๋ ํน์ ์ฑ๋๋ก ๋ฉ์ธ์ง๋ฅผ ์ ์ก ํ ์ฑ๊ณต ๋๋ ์คํจํ์ ๋ ๋ก๊ทธ๋ฅผ ๋จ๊ธฐ๋ ๊ฐ๋จํ ๋ก์ง์ ๋๋ค.
@Injectable()
export class NotificationService {
private webhookUrl: string;
private readonly webhookName = 'webhook profile name';
constructor(
private readonly configService: ConfigService,
private readonly httpService: HttpService,
private readonly logger: LoggerService,
) {
this.logger.setContext(NotificationService.name);
this.webhookUrl = this.configService.get<ServerConfig>(SERVER_CONFIG_TOKEN).webhookUrl;
}
async reportError(errorContent: ErrorContent) {
const message = {
// ์๋ต...
};
try {
await this.httpService.axiosRef.post(this.webhookUrl, message);
this.logger.log('Error Notification is sent to the Discord channel.');
} catch (error) {
if (error instanceof AxiosError) {
this.logger.warn(error.message, error.response.data);
}
}
}
}
2. ํ ์คํธ ์ฝ๋ (v1)
reportError() ํจ์๊ฐ ์ ๋์ํ๋์ง ํ์ธํ๊ธฐ ์ํด ํ ์คํธ ์ฝ๋๋ฅผ ์๋์ ๊ฐ์ด ์์ฑํ์ต๋๋ค.
// v1-notification.service.spec.ts
describe('NotificationService', () => {
let notificationService: NotificationService;
let httpService: HttpService;
let logger: LoggerService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
NotificationService,
{
provide: HttpService,
useValue: {
axiosRef: {
post: jest.fn(),
},
},
},
{
provide: LoggerService,
useValue: {
setContext: jest.fn(),
log: jest.fn(),
warn: jest.fn(),
},
},
{
provide: ConfigService,
useValue: {
get: jest.fn(() => ({ webhookUrl: 'test_url' }))
},
},
],
}).compile();
notificationService = module.get<NotificationService>(NotificationService);
httpService = module.get<HttpService>(HttpService);
logger = module.get<LoggerService>(LoggerService);
});
it('should be defined', () => {
expect(notificationService).toBeDefined();
});
test('๋ฉ์์ง ์ ์ก์ ์ฑ๊ณตํ๋ฉด, info ๋ ๋ฒจ์ ๋ก๊ทธ๋ฅผ ๋จ๊ธด๋ค.', async () => {
// given
const postSpy = jest
.spyOn(httpService.axiosRef, 'post')
.mockResolvedValue({ status: 204 });
const logSpy = jest.spyOn(logger, 'log');
// when
await notificationService.reportError({
method: 'GET',
url: '/api/test',
status: 500,
timestamp: '2024-12-01T12:00:00Z',
userId: 'user-123',
trace: 'Stack trace details...',
context: 'AllExceptionFilter',
});
// then
expect(postSpy).toHaveBeenCalledTimes(1);
expect(logSpy).toHaveBeenCalledTimes(1);
});
test('๋ฉ์์ง ์ ์ก์ ์คํจํ๋ฉด, warn ๋ ๋ฒจ์ ๋ก๊ทธ๋ฅผ ๋จ๊ธด๋ค.', async () => {
// given
const postSpy = jest
.spyOn(httpService.axiosRef, 'post')
.mockRejectedValue(
new AxiosError('Request failed', null, null, null, { data: 'Error details' } as AxiosResponse),
);
const warnSpy = jest.spyOn(logger, 'warn');
// when
await notificationService.reportError({
method: 'GET',
url: '/api/test',
status: 500,
timestamp: '2024-12-01T12:00:00Z',
userId: 'user-123',
trace: 'Stack trace details...',
context: 'AllExceptionFilter',
});
// then
expect(postSpy).toHaveBeenCalledTimes(1);
expect(warnSpy).toHaveBeenCalledTimes(1);
});
});
ํ
์คํธ ์์ฒด๋ ๊ต์ฅํ ๋จ์ํ์ง๋ง beforeEach()๋ฅผ ์์ฑํ๋ ๋ถ๋ถ์ด... ๋๋ฌด ๊ท์ฐฎ์์ต๋๋ค.
๋น์ฆ๋์ค ๋ก์ง๊ณผ ๋น๊ตํ๋ฉด์ ์์กด์ฑ์ ๋ชจ์ ๊ฐ์ฒด๋ก ์์ฑํด์ผ ํ๋๋ฐ, ์ฌ์ค์ TDD๊ฐ ๋ถ๊ฐ๋ฅํ ๊ฒ์ด ์๋๊ฐ ์ถ์์ต๋๋ค.
๋น์ฆ๋์ค ๋ก์ง ๋ด๋ถ์ ์ด๋ค ํจ์์ ๊ฐ๋ค์ด ์ฐ์ผ์ง ๋ชจ๋ ์๊ณ ์์ด์ผ ํ๋๊น์.
์ด ์์
์ ๋จ์ํํ ์ ์๋ ๋ฐฉ๋ฒ์ ์ฐพ์๋ณด๋ค๊ฐ, @golevelup/ts-jest
๋ผ๋ ๋ชจ๋์ ๋ฐ๊ฒฌํ์ต๋๋ค.
NestJS ๊ณต์๋ฌธ์๋ฅผ ๋ณด๋ฉด ์๋์ ๊ฐ์ ๋ด์ฉ์ด ์์ต๋๋ค. โฌ๏ธ
useMocker๋ฅผ ํตํด @golevelup/ts-jest์ createMock์ด๋ผ๋ mock factory๋ฅผ ์ ๋ฌํ ์ ์๋ค๊ณ ํฉ๋๋ค.
@golevelup/ts-jest ์์ ์ ๊ณตํ๋ ๊ธฐ๋ฅ
ํ์ ์์ฑ์ด ๋๋ฌด ๋ง์์ ๋ชจ์ ๊ฐ์ฒด๋ฅผ ๋ง๋ค๊ธฐ ์ด๋ ค์ด ๊ฒฝ์ฐ ์ ์ฌ์ฉํ ์ ์๋ NestJS์ฉ ์ ํธ๋ฆฌํฐ ํจ์ createMock์ ์ ๊ณตํฉ๋๋ค.
๋ชจ๋ ํ์ ์์ฑ์ jest.fn()์ผ๋ก ๋ชจ์ํ์ฌ ๋ชจ์ ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ค๋ค๊ณ ํ๋ค์.
NestJS์ HttpService๋ฅผ ๋ชจ์ํ ๋ ์ฌ์ฉํ๋ฉด ๋ฑ ์ข๊ฒ ๋ค๋ ์๊ฐ์ด ๋ค์์ต๋๋ค.
createMock ์ ์ฉํด๋ณด๊ธฐ
์ ์ฉ ๋ฐฉ๋ฒ์ ๊ต์ฅํ ๊ฐ๋จํฉ๋๋ค.
@nestjs/testing ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ํ ์คํ ๋ชจ๋์ ์์ฑํ ๋,
useMocker() ํจ์๋ก ์์กด์ฑ์ ๋ํ ๋ชจ์ ๊ฐ์ฒด๋ฅผ ์ ๋ฌ๋ง ํด์ฃผ๋ฉด ๋๋๋ฐ์.
// v2-notification.service.spec.ts
describe('NotificationService', () => {
let notificationService: NotificationService;
let httpService: HttpService;
let logger: LoggerService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [NotificationService],
})
.useMocker(createMock) // โฌ
๏ธ Here!
.compile();
notificationService = module.get<NotificationService>(NotificationService);
httpService = module.get<HttpService>(HttpService);
logger = module.get<LoggerService>(LoggerService);
});
it('should be defined', () => {
expect(notificationService).toBeDefined();
});
test('๋ฉ์์ง ์ ์ก์ ์ฑ๊ณตํ๋ฉด, info ๋ ๋ฒจ์ ๋ก๊ทธ๋ฅผ ๋จ๊ธด๋ค.', async () => {
// ์๋ต...
});
test('๋ฉ์์ง ์ ์ก์ ์คํจํ๋ฉด, warn ๋ ๋ฒจ์ ๋ก๊ทธ๋ฅผ ๋จ๊ธด๋ค.', async () => {
// ์๋ต...
});
});
ํน์ ์์กด์ฑ๋ง createMock์ผ๋ก ๋ชจ์ํ๊ณ ์ถ๋ค๋ฉด ์๋์ฒ๋ผ ์์ฑํ๋ฉด ๋ฉ๋๋ค. โฌ๏ธ
const module = await Test.createTestingModule({
providers: [
NotificationService,
{
provide: HttpService,
useValue: createMock<HttpService>(),
},
{...}
],
}).compile();
๊ฒฐ๊ณผ
ํ ์คํธ ๋ด์ฉ์ ๋ณ๊ฒฝํ์ง ์๊ณ createMock์ ์ ์ฉํ์ ๋๋ ๋ง์ฐฌ๊ฐ์ง๋ก ๋ชจ๋ ํ ์คํธ๊ฐ ํต๊ณผํ์ต๋๋ค.
์ฐธ๊ณ
- https://docs.nestjs.com/fundamentals/testing#auto-mocking
- https://youtu.be/GU7U35lbBS0?si=7ajGTSve4ALiue-n
- https://blog.outsider.ne.kr/1275
- https://medium.com/@ledevnovice/simplify-your-unit-tests-with-jest-using-createmock-59300a063b5b
'Dev > ํ ์คํธ' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Jest] Jest ํจ์ ์คํ ์์ (0) | 2024.07.29 |
---|