[NestJS] Unit Test 하기

곽태민·2023년 6월 7일
0

NestJS

목록 보기
5/8

개요


전 회사에서 테스트 코드를 짰을 때, E2E 테스트는 많이 했다고 생각하는데 단위 테스트를 해본 경험이 많이 없었고, 또한 메인 개발 언어도 Typescript가 아니였기 떄문에 NestJS에서 단위 테스트를 하는 방법을 공부 좀 해보려고 했다.

(Typescript를 더 선호하는 나에게는 전 회사에서 Typescript로 개발을 많이 못했던게 좀 아쉬웠던 부분이라고 생각을 한다.)

Unit Test


NestJS에서 Unit Test를 하기전에 개념을 먼저 짚고 넘어가려고 한다. 다른 포스트에도 정리를 해놓은게 있지만, 한번 더 주입을 시키고자 개념을 정리하려고 한다.

Unit Test는 프로그래밍을 할 때 소스코드의 특정 모듈(메서드)이 의도된 대로 정확히 작동하는지 확인을 하는 절차로, 코드 작성한 메서드들에 대해서 테스트 케이스를 작성하는 것을 의미한다.

Unit Test 장점

Unit Test를 진행하면 하나의 기능을 독립적으로 테스트를 해서 코드 변경으로 인한 문제가 발생해도 짧은 시간안에 문제 해결을 할 수 있다.

또한 새로운 기능 추가 시 수시로 빠르게 테스트 할 수 있으며, 리팩토링 시 안정성을 확보할 수 있다. 그리고 테스팅에 대한 시간 및 비용절감이 가능하며, 코드에 대한 문서가 되기도 한다.

좋은 Unit Test

좋은 Unit Test를 하기 위해서는 1개의 테스트 함수에 대해서는 assert를 최소화 해야하고, 1개의 테스트 함수에는 1가지 개념만 테스트를 해야한다.

위에 정리한 글 외에 좋고 깔끔한 테스트 코드를 작성하는 규칙이 있는데 FIRST라는 5가지 규칙이 존재한다.

  • Fast: 테스틑 빠르게 동작하여 자주 실행시킬 수 있어야한다.
  • Independent: 각 테스트들은 독립적이고, 서로 의존해서는 안된다.
  • Repeatable: 어느 환경에서도 반복이 가능해야한다.
  • Self-Validating: 테스트는 성공 또는 실패로 Boolean 값으로 결과를 나타내어 자체적으로 검증되어야 한다.
  • Timely: 테스트는 적시에 즉, 테스트하려는 실제 코드를 구현하기 직전에 구현해야 한다.

NestJS에서 간단한 Unit Testing


우선 nest-cli를 통하여 어플리케이션을 만들었다고 가정하고, 본인은 Cats라는 도메인을 만들어서 간단한 단위 테스트를 진행했다.

참고 nest g s cats를 하면 테스트 코드를 작성할 수 있는 cats.service.spec.ts 파일이 생성된다.

// cats.service.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { CatsService } from './cats.service';

describe('CatsService', () => {
	let service: CatsService;
  
  	beforEach(async () => {
    	const module: TestingModule = await Test.createTestingModule({
        	providers: [CatsService],
        }).compile();
      
      	service = module.get<CatsService>(CatsService);
    });
  
  	it('should be defined', () => {
    	expect(service).toBeDefined();
    });
});

위 코드 처럼 기본적으로 틀을 만들어준채로 파일이 생성된다. cats.service.ts의 코드는 아래와 같다.

// cats.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import { CatsRepository } from './cats.repository';
import { InjectRepository } from '@nestjs/typeorm';
import { MyPaginationQuery } from '../base/pagination-query';
import { Pagination, paginate } from 'nestjs-typeorm-paginate';
import { Cats } from './entities/cats.entity';
import { CATS_EXCEPTION } from '../exceptions/error-code';
import { CreateCatDto } from './dto/create-cat.dto';

@Injectable()
export class CatsService {
  constructor(
    @InjectRepository(CatsRepository) private catsRepository: CatsRepository,
  ) {}

  /**
   * 고양이 전체 조회
   * @param options
   * @returns
   */
  async findAllByCats(options: MyPaginationQuery): Promise<Pagination<Cats>> {
    return paginate(await this.catsRepository, options);
  }

  /**
   * 특정 고양이 조회
   * @param catsId
   * @returns
   */
  async findOneById(catsId: number): Promise<Cats> {
    const cat = await this.catsRepository.findOneById(catsId);

    if (!cat) {
      throw new NotFoundException(CATS_EXCEPTION.CAT_NOT_FOUND);
    }

    return cat;
  }

  /**
   * 고양이 생성
   * @param createCatDto
   * @returns
   */
  async createCat(createCatDto: CreateCatDto): Promise<Cats> {
    return await this.catsRepository.createCat(createCatDto);
  }

  /**
   * 고양이 수정
   * @param catsId
   * @returns
   */
  async update(catsId: number, cats: Cats): Promise<Cats> {
    const cat = await this.findOneById(catsId);

    cat.name = cats.name;

    return await this.catsRepository.updateCat(cat);
  }

  /**
   * 고양이 삭제
   * @param catsId
   * @returns
   */
  async delete(catsId: number): Promise<void> {
    const cat = await this.findOneById(catsId);

    await this.catsRepository.deleteCat(cat.catsId);
  }
}

CatsService에서는 CatsRepository를 constructor로 받아와서 DB에 직접적으로 코딩을 하지않고, 비즈니스 로직만 구현을 위한 코딩을 했다.

import { Injectable } from '@nestjs/common';
import { DataSource, Repository } from 'typeorm';
import { Cats } from './entities/cats.entity';
import { CreateCatDto } from './dto/create-cat.dto';

@Injectable()
export class CatsRepository extends Repository<Cats> {
  constructor(private readonly dataSource: DataSource) {
    super(Cats, dataSource.createEntityManager());
  }

  /**
   * 고양이 생성
   * @param createCatDto
   * @returns
   */
  async createCat(createCatDto: CreateCatDto): Promise<Cats> {
    const { name } = createCatDto;
    const createCat = await this.create({ name });

    return await this.save(createCat);
  }

  /**
   * 특정 고양이 조회
   * @param catsId
   * @returns
   */
  async findOneById(catsId: number): Promise<Cats> {
    const cat = await this.findOne({ where: { catsId } });

    return cat;
  }

  /**
   * 수정 시 사용할 method
   * @param cats
   * @returns
   */
  async updateCat(cats: Cats): Promise<Cats> {
    return await this.save(cats);
  }

  /**
   * 고양이 삭제
   * @param catsId 
   */
  async deleteCat(catsId: number): Promise<void> {
    await this.delete(catsId);
  }
}

이렇게 하고 이제 service에 대한 단위 테스트를 진행해볼 것이다. (controller 코드는 생략.)

본인은 Unit Test를 할 때 실전이라 생각하고 수시로 테스트 코드를 돌릴 것이라 가정하에 진행을 했기 때문에 DB에 직접적으로 insert, select, delete, update 등을 하지 않고 mocking을 할 것이다.

mocking은 fake DB를 만들어서 DB에서 조회, 생성 등등을 한다고 생각하면 될 것 같다. (다음 포스트에서 정리 할 예정)

우선은 cats.service.spec.ts에서 mockRepository를 만들어 줄 것이다.

import { Test, TestingModule } from '@nestjs/testing';
import { CatsService } from './cats.service';
import { CatsRepository} from './cats.repository';

const mockReepository = {
	findAllByCats: jest.fn(),
  	findOneById: jest.fn(),
  	createCat: jest.fn(),
  	updateCat: jest.fn(),
  	deleteCat: jest.fn(),
};

describe('CatsService', () => {
	let service: CatsService;
  	let repository: CatsRepository;
  
  	beforeEach(async () => {
    	const module: TestingModule = await Test.createTestingModule({
        	providers: [
            	CatsService,
              	{
                	provide: CatsRepository,
                  	useValue: mockRepository,
                },
            ],
        }).compile();
      
      	service = module.get<CatsService>(CatsService);
      	repository = module.get<CatsRepository>(CatsRepository);
      
      	it('should be defined', () => {
    	expect(service).toBeDefined();
    });
    });
});

위 코드에서 선언한 mockRepository를 사용하여 DataSource import를 하지 않고, CatsRepository를 사용할 수 있다. CatsRepositoryRepository를 extends하기 때문에 DataSource가 필요하지만 mocking으로 fake를 줬다.

이제 이렇게 설정하고 각 메서드에 대해서 테스트 코드를 작성해주면 되는데, 간단한 예시로 createCat을 진행을 하려고한다.

// cats.service.spec.ts

describe('createCat', () => {
	it('create new cat', async () => {
    	const createCatDto: CreateCatDto = { name: 'newCat' };
      	const savedCat: Cats = { catsId: 1, name: 'newCat' };
      
      	jest.spyOn(repository, 'createCat').mockResolvedValue(savedCat);
      
      	const result = await service.createCat(createCatDto);
      
      	expect(result).toEqual(savedCat);
      	expect(repository.createcat).toHaveBeenCalledWith(createCatDto);
    });
});

NestJS는 테스트 라이브러리를 기본적으로 Jest를 사용하는데 Jest에서 기본적으로 제공하는 메서드들을 이용해서 mocking을 할 수 있다.

mockResolvedValue(return값) 메서드는 이 메서드를 이용해서 가짜 함수가 어떤 값을 리턴할지 설정할 수 있다. 괄호안에 Promise가 resolve하는 값을 넣게 되면 비동기 함수를 만들 수 있다.

toEqual 은 테스트 코드를 작성하다보면 자주 보이지만 toHaveBeenCalledWith() 은 좀 생소해서 찾아보게 됐다.

해당 Matcher는 Jest에서 Mock 함수를 사용해 테스트할 때, 해당 Mock 함수의 호출 시 특정 인자 값과 함께 사용되었는지에 대한 테스트 Matcher이다.

(Matcher는 Jest에서 expect통해 값을 검증할 때, 해당 값을 검증하기 위한 도구다.)

마무리


이렇게 Unit Test를 진행을 해봤는데, E2E 테스트는 많이 해봤지만 Unit Test는 많이는 안해봐서 이해하는데 좀 오래걸렸다...

service도 Unit Test를 진행했는데, Controller나 repository도 테스트를 해야하지 않을지 생각이 들었지만 모든 로직을 테스트할 필요는 없다고 들었다.

이유는 100%를 달성했다고 결함이 없는 것도 아니며, 모든 코드를 한번씩 실행시킨 것만 보장할 뿐이라 의미 있는 테스트를 작성하는데 집중을 해야한다고 들었다.

다음은 Jest와 Mocking 관련하여 포스트를 정리를 해보려고한다. Jest Matcher들은 Docs에 많이 나와 있긴하지만 그래도 기록하는게 습관이 되어야하니까..

profile
Node.js 백엔드 개발자입니다!

0개의 댓글