๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

Dev/ํ…Œ์ŠคํŠธ

NestJS์—์„œ createMock์œผ๋กœ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๊ฐ„๋‹จํ•˜๊ฒŒ ์ž‘์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•

 

 

 

๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ๋” ๊ฐ„๋‹จํ•˜๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด ์—†์„๊นŒ...? ๐Ÿค”



 

 

 

๋‹จ์œ„ ํ…Œ์ŠคํŠธ(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๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

https://docs.nestjs.com/fundamentals/testing#auto-mocking

 

 

 

 

@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์„ ์ ์šฉํ–ˆ์„ ๋•Œ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ํ†ต๊ณผ ๐Ÿ‘

 

 

์ฐธ๊ณ 

 

 

'Dev > ํ…Œ์ŠคํŠธ' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[Jest] Jest ํ•จ์ˆ˜ ์‹คํ–‰ ์ˆœ์„œ  (0) 2024.07.29