테스트 코드 공부하기! Jest
멘토님 과제하기!
우선 내가 구현한 모듈에 테스트 코드 적용해 보기
기본 구조 잡았음!
핵심은 Mock 객체를 주입하는 것!
import { Test, TestingModule } from '@nestjs/testing';
import { MailController } from '../mail.controller';
import { MailService } from '../mail.service';
describe('MailController', () => {
let controller: MailController;
let mockMailService: Partial<MailService>;
beforeEach(async () => {
mockMailService = {
sendFeedbackMail: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
controllers: [MailController],
providers: [
{
provide: MailService,
useValue: mockMailService,
},
],
}).compile();
controller = module.get<MailController>(MailController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
it('메일이 발송이 성공합니다.', async () => {
const feedbackMailDto = { feedback: 'Test feedback' };
const req = { user: { sub: '1234' } };
// 에러가 발생하지 않는지 확인
await expect(
controller.sendFeedbackMeil(req, feedbackMailDto),
).resolves.not.toThrow();
// sendFeedbackMail 메서드가 호출되었는지 확인
expect(mockMailService.sendFeedbackMail).toHaveBeenCalledWith(
feedbackMailDto,
'1234',
);
});
it('피드백(본문)이 없을 때 에러가 발생합니다.', async () => {
const feedbackMailDto = { feedback: '' };
const req = { user: { sub: '1234' } };
await expect(
controller.sendFeedbackMeil(req, feedbackMailDto),
).rejects.toThrow('Feedback is required');
});
});
2가지 케이스에 대해서 테스트
package.json에 설정 변경
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node",
// 이거 추가함!
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/$1"
}
}
import * as nodemailer from 'nodemailer';
import { Test, TestingModule } from '@nestjs/testing';
import { MailService } from '../mail.service';
jest.mock('nodemailer');
const createTransportMock = nodemailer.createTransport as jest.MockedFunction<
typeof nodemailer.createTransport
>;
describe('MailService', () => {
let service: MailService;
let sendMailMock: jest.Mock;
beforeEach(async () => {
sendMailMock = jest.fn();
createTransportMock.mockReturnValue({ sendMail: sendMailMock });
const module: TestingModule = await Test.createTestingModule({
providers: [MailService],
}).compile();
service = module.get<MailService>(MailService);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('sendMail 메서드가 호출되었습니다.', async () => {
const to = 'test@example.com';
const subject = 'Test subject';
const html = '<div>Test html</div>';
await service.sendMail(to, subject, html);
expect(sendMailMock).toHaveBeenCalledWith({
from: process.env.MAIL_USER,
to,
subject,
html,
});
});
it('sendMail 메서드가 에러를 던집니다.', async () => {
const to = 'test@example.com';
const subject = 'Test subject';
const html = '<div>Test html</div>';
const error = new Error('Error sendMail');
sendMailMock.mockRejectedValue(error);
await expect(service.sendMail(to, subject, html)).rejects.toThrow(error);
});
it('sendWelcomeMail 메서드가 호출되었습니다.', async () => {
const to = 'test@example.com';
const userId = 'testUser';
await service.sendWelcomeMail(to, userId);
expect(sendMailMock).toHaveBeenCalled();
});
it('sendResetPasswordMail 메서드가 호출되었습니다.', async () => {
const to = 'test@example.com';
const userId = 'testUser';
const token = '9999';
await service.sendResetPasswordMail(to, userId, token);
expect(sendMailMock).toHaveBeenCalled();
});
it('sendFeedbackMail 메서드가 호출되었습니다.', async () => {
const feedbackMailDto = { feedback: 'Test feedback' };
const userId = 'testUser';
await service.sendFeedbackMail(feedbackMailDto, userId);
expect(sendMailMock);
});
});
테스트 코드의 과정이 테스트 하려는 레이어의 하위 레이어를 Mock 객체로 만들어서 주입하고, 하위 레이어와 무관하게 테스트하려는 레이어의 로직만 테스트하는 것 같음. 즉, 하위 Mock 레이어는 결과를 내가 의도한 대로 가짜(Mock)으로 만듦.
it('sendMessage 메서드가 message 필드가 없는 경우 에러를 반환합니다.', async () => {
const sendTelegramDto = {
message: '',
};
await expect(controller.sendMessage(sendTelegramDto)).rejects.toThrow(
'Required message field is missing.',
);
});
@Post()
async sendMessage(@Body() sendTelegramDto: SendTelegramDto): Promise<void> {
if (!sendTelegramDto.message) {
throw new Error('Required message field is missing.222');
}
return this.telegramService.sendMessage(sendTelegramDto);
}
이 상황에서는 테스트가 실패해야 한다.
그런데 테스트에 통과한다.
controller 에러메시지 맨 뒤에 숫자가 들어가면 테스트가 통과하고, 그 외에는 실패한다. 이유는?
Jest 문서에 따르면, .toThrow()는 주어진 문자열이나 정규 표현식과 완전히 일치하는 에러 메시지를 기대합니다. 따라서, 에러 메시지의 미묘한 차이도 테스트 결과에 영향을 줄 수 있습니다.
이 특정 케이스에서 테스트가 통과하는 것은 다음과 같은 이유 때문일 수 있습니다:
Jest 버전 또는 구성 차이: 사용 중인 Jest의 버전이나 테스트 환경의 구성에 따라 .toThrow() 메서드의 동작이 약간 다를 수 있습니다. 예외 메시지의 부분적 일치를 허용하는 동작은 Jest의 특정 버전이나 구성에 의해 발생할 수 있습니다.
테스트 환경의 비동기 처리 방식: 비동기 함수에서 발생하는 예외를 처리할 때, Jest의 비동기 처리 방식이 예외 캐치와 검증에 영향을 줄 수 있습니다. .rejects.toThrow() 구문은 비동기 예외를 적절히 처리하도록 설계되었지만, 테스트 코드의 실행 컨텍스트나 프로미스의 거부(rejection) 방식에 따라 결과가 달라질 수 있습니다.
문자열 검증의 내부 로직: Jest가 내부적으로 예외 메시지를 검증할 때 사용하는 로직이 예외 메시지의 전체 일치 대신 부분 일치를 허용할 수 있습니다. 이는 Jest의 구현 세부사항에 따라 다르며, 공식 문서에서 명시적으로 설명되지 않은 동작일 수 있습니다.
테스트 공부가 필요할 것 같아서 책 삼.
https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=310632260
let mockTelegramService: Partial<TelegramService>;
이건 의 타입의 모든 프로퍼티를 Optional하게 만듦!
그래서 일부만 사용할 수 있는 것
배너 광고 띄워 봄.