[크래프톤 정글 3기] 2/8(목) TIL

ClassBinu·2024년 2월 8일
0

크래프톤 정글 3기 TIL

목록 보기
110/120

테스트 코드 공부하기! Jest
멘토님 과제하기!

Test

우선 내가 구현한 모듈에 테스트 코드 적용해 보기

Mail Test

controller test

기본 구조 잡았음!
핵심은 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가지 케이스에 대해서 테스트

  • 메일 발송이 성공했는지
  • 피드백 본문이 비어있으면 에러가 발생하는지

Jest가 절대 경로 문제

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"
    }
  }

service test

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)으로 만듦.

jest의 이상한 동작

테스트 코드

  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 에러메시지 맨 뒤에 숫자가 들어가면 테스트가 통과하고, 그 외에는 실패한다. 이유는?

GPT 분석

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하게 만듦!
그래서 일부만 사용할 수 있는 것

1차 배포

https://storifyai.vercel.app

배너 광고 띄워 봄.

0개의 댓글