Nest.js 테스트 코드 작성

SEUNGJUN·2024년 6월 7일
post-thumbnail

테스트 코드를 사용하는 이유는 여러가지가 있다.

1. 버그 방지

  • 테스트 코드는 예상치 못한 버그를 미리 발견할 수 있게 도와준다.

2. 안정성 향상

  • 코드 변경 시 기존 기능이 정상적으로 작동하는지 확인할 수 있다.

3. 문서화

  • 테스트 코드는 코드의 사용 방법과 예상 동작을 문서화하는 효과가 있다.

4. 개발 속도 향상

  • 코드의 변경이 다른 부분에 미치는 영향을 빠르게 확인할 수 있어 개발 속도를 높일 수 있다.

Nest.js에서 테스트 코드 작성하기

1. 프로젝트 생성

우선, 새로운 Nest.js 프로젝트를 생성한다.

nest new my-nest-project
cd my-nest-project

2. 기본 테스트 파일 구조

Nest.js 프로젝트를 생성하면 기본적으로 src 폴더 안에 app.controller.spec.ts 파일이 생성된다. 이 파일은 기본적으로 테스트 코드를 포함하고 있다.

import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';

describe('AppController', () => {
  let appController: AppController;

  beforeEach(async () => {
    const app: TestingModule = await Test.createTestingModule({
      controllers: [AppController],
      providers: [AppService],
    }).compile();

    appController = app.get<AppController>(AppController);
  });

  describe('root', () => {
    it('should return "Hello World!"', () => {
      expect(appController.getHello()).toBe('Hello World!');
    });
  });
});

3. 테스트 실행

테스트를 실행하려면 다음 명령어를 사용한다.

npm run test

4. 간단한 서비스 테스트 코드 작성

새로운 서비스를 추가하고, 그 서비스의 테스트 코드를 작성 한다면

  • 서비스 생성
nest g service my
  • my.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class MyService {
  getHello(): string {
    return 'Hello from MyService!';
  }
}
  • my.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { MyService } from './my.service';

describe('MyService', () => {
  let service: MyService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [MyService],
    }).compile();

    service = module.get<MyService>(MyService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  it('should return "Hello from MyService!"', () => {
    expect(service.getHello()).toBe('Hello from MyService!');
  });
});

5. 컨트롤러 테스트 코드 작성

컨트롤러를 테스트하는 코드도 비슷한 방식으로 작성할 수 있다.

  • 컨트롤러 생성
nest g controller my

my.controller.ts

import { Controller, Get } from '@nestjs/common';
import { MyService } from './my.service';

@Controller('my')
export class MyController {
  constructor(private readonly myService: MyService) {}

  @Get()
  getHello(): string {
    return this.myService.getHello();
  }
}

my.controller.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { MyController } from './my.controller';
import { MyService } from './my.service';

describe('MyController', () => {
  let controller: MyController;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [MyController],
      providers: [MyService],
    }).compile();

    controller = module.get<MyController>(MyController);
  });

  it('should be defined', () => {
    expect(controller).toBeDefined();
  });

  it('should return "Hello from MyService!"', () => {
    expect(controller.getHello()).toBe('Hello from MyService!');
  });
});

Nest.js에서 테스트 코드를 작성하면 코드의 안정성을 높이고, 버그를 사전에 방지하며, 개발 속도를 높일 수 있다. Jest를 사용하여 테스트 코드를 쉽게 작성할 수 있으며, 테스트 코드는 서비스와 컨트롤러를 포함한 다양항 부분에 대해 작성할 수 있다.

테스트 코드 주요 개념

MyController의 동작 테스트 코드에 대해서 설명을 하자면

1. describe

describe는 테스트 스위트(test suite)를 정의하는 함수이다. 하나의 테스트 스위트는 관련된 여러 개의 테스트 케이스를 그룹화 한다. 첫 번째 인자는 테스트 스위트의 이름(설명)이고, 두 번째 인자는 테스트 케이스들을 포함하는 함수 이다.

describe('MyController', () => {
  // 테스트 케이스들이 여기에 포함됩니다.
});

2. beforeEach / afterEach

beforeEach는 각 테스트 케이스가 실행되기 전에 공통적으로 실행되어야 하는 설정 작업을 정의 한다. 이 예제에서는 beforeEach를 사용하여 TestingModule을 컴파일하고, MyController의 인스턴스를 가져온다.

beforeEach(async () => {
// 각 테스트 케이스가 실행되기 전에 실행
  const module: TestingModule = await Test.createTestingModule({
    controllers: [MyController],
    providers: [MyService],
  }).compile();

  controller = module.get<MyController>(MyController);
});
afterEach(() => {
  // 각 테스트 케이스가 실행된 후에 실행
});

3. it / test

it / test 는 개별 테스트 케이스를 정의하는 함수이다. 첫 번째 인자는 테스트 케이스의 설명이고, 두 번째 인자는 테스트가 수행될 함수이다. it 블록 안에는 테스트 대상 코드와 그 코드의 결과를 검증하는 코드가 들어간다.

it('should be defined', () => {
  expect(controller).toBeDefined();
});

it('should return "Hello from MyService!"', () => {
  expect(controller.getHello()).toBe('Hello from MyService!');
});

5. beforeAll / afterAll

테스트 스위트가 실행되기 전과 후에 각각 한 번씩 실행되는 함수

beforeAll(() => {
  // 전체 테스트 스위트가 실행되기 전에 한 번 실행
});

afterAll(() => {
  // 전체 테스트 스위트가 실행된 후에 한 번 실행
});

6. expect

expect는 Jest의 단언(assertion) 함수이다. 테스트 대상의 값을 받아서, 그 값이 기대하는 조건을 만족하는지 확인한다.

expect(controller).toBeDefined();

매처 함수들

  • toBe: 실제 값이 기대한 값과 정확히 일치하는지 확인.

  • toEqual: 객체나 배열과 같은 값의 구조가 일치하는지 확인.

  • toBeTruthy: 값이 참인지 확인.

  • toBeFalsy: 값이 거짓인지 확인.

  • toContain: 배열이나 문자열에 특정 값이 포함되어 있는지 확인.

  • toHaveLength: 배열이나 문자열의 길이가 특정 값인지 확인.

  • toHaveProperty: 객체가 특정 프로퍼티를 가지고 있는지 확인.

  • toMatch: 문자열이 특정 정규식과 일치하는지 확인.

  • toThrow: 함수가 예외를 던지는지 확인.

expect(value).toEqual(expectedValue);
expect(array).toContain(element);
expect(string).toMatch(/pattern/);
expect(fn).toThrow();

7. tobe

tobe는 Jest의 매처(matcher) 함수 중 하나로, expect와 함께 사용되어 실제 값이 기대하는 값과 정확히 일치하는지 확인한다.

expect(controller.getHello()).toBe('Hello from MyService!');

8. Mock 함수

Jest에서 mock 함수를 사용하여 함수 호출과 결과를 시뮬레이션할 수 있다.

  • jest.fn: 기본 mock 함수 생성.
  • jest.spyOn: 기존 객체 매서드를 감시하고 모킹.
const mockFn = jest.fn();
mockFn();
expect(mockFn).toHaveBeenCalled();

const obj = { method: jest.fn() };
obj.method();
expect(obj.method).toHaveBeenCalled();
const obj = { method: () => 'real implementation' };
const spy = jest.spyOn(obj, 'method').mockImplementation(() => 'mock implementation');
expect(obj.method()).toBe('mock implementation');
spy.mockRestore();

테스트 코드 작성이 중요한가?

테스트 코드를 모든 상황에서 작성하는 것이 이상적이지만, 현실적으로 프로젝트의 규모와 성격, 개발 리소스 등에 따라 우선순위를 정하고 필요한 부분부터 작성하는 것이 좋다.

테스트 코드 작성이 주요한 경우

1. 핵심 비즈니스 조릭

  • 애플리케이션의 핵심 기능을 담당하는 로직은 반드시 테스트해야 한다. 이는 버그 발생 시 큰 영향을 미치기 때문이다.

2. 복잡한 로직

  • 복잡한 알고리즘이나 로직이 포함된 경우, 테스트 코드를 작성하여 예상하지 못한 버그를 방지 할 수 있다.

3. 빈번하게 수정되는 코드

  • 자주 변경되는 코드에는 테스트를 작성하여 코드 변경 시 기존 기능이 정상적으로 동작하는지 확인 할수 있다.

4. 외부 API와의 통신

  • 외부 API와의 통신 로직은 테스트하여 정상적으로 데이터가 주고받는지 확인하는 것이 중요하다.

5. 보안 관련 기능

  • 인증, 권한 부여와 같은 보안 관련 기능은 반드시 테스트 해야 한다.

테스트 코드 작성이 덜 중요한 경우

1. 간단한 코드

  • 매우 단순한 기능이나 로직은 테스트 코드 작성의 우선순위가 낮을 수 있다.

2. 빠르게 변하는 초기 프로토타입

  • 초기 개발 단계의 프로토타입에서는 테스트 코드 작성보다 기능 구현에 집중할 수 있다. 하지만 일정 수준의 안정성이 확보되면 테스트 코드를 작성하는 것도 좋다.

3. UI 코드

  • 프론트엔드 UI 코드의 경우, 시각적인 요소나 사용자 인터페이스에 대한 테스트는 도구를 활용한 자동화 테스트나 수동 테스트로 대처될 수 있다.

테스트 코드 작성의 우선순위

1. 핵심 기능 우선

  • 애플리케이션의 핵심 기능에 대한 테스트를 먼저 작성한다.

2. 에러가 발생할 가능성이 높은 부분

  • 복잡한 로직이나 자주 변경되는 코드에 대해 우선적으로 테스트를 작성한다.

3. 테스트 커버리지 확대

  • 중요한 부분에 대한 테스트가 완료되면, 점차적으로 다른 부분에 대한 테스트 커버리지를 확대해 나간다.

모든 코드에 대해 테스트 코드를 작성하는 것이 이상적이지만, 현실적으로 우선순위를 정해 중요한 부분부터 작성하는 것이 효율적이다. 핵심 비즈니스 로직, 복잡한 로직, 자주 변경되는 코드, 외부 API 통신, 보안 관련 기능 등에는 우선적으로 테스트 코드를 작성하고, 프로젝트의 상황에 따라 점진적으로 테스트 커버리지를 확대해 나가는 것이 좋다.

profile
RECORD DEVELOPER

0개의 댓글