[NestJS] Unit test

김형주·2021년 10월 9일
3

Backend Study

목록 보기
17/19
post-thumbnail
post-custom-banner

Unit test (단위 테스트)

NestJS는 내장된 종속성 주입을 사용해서 쉽게 테스트 코드를 작성할 수 있도록 도와준다! 종속성 주입은 일반적으로 클래스가 아닌 인터페이스를 기반으로하지만, TypeScript에서 인터페이스는 런타임이 아닌 컴파일 타임에만 존재한다.(런타임에는 인터페이스는 증발함.) 그렇기 때문에 타입에 대한 100% 신뢰성을 부여하기 어렵다. NestJS에서는 클래스 기반 주입을 사용하는 것이 일반적이다.

import { Test, TestingModule } from '@nestjs/testing';

describe('UserService', () => {
  let userService: UserService;
  let userRepository: UserRepository;

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

    userService = module.get<UserService>(UserService);
    userRepository = module.get<UserRepository>(UserRepository);
  });
}

기존에 Express에서는 sinon과 같은 패키지를 사용해서 테스트하려는 Service를 모킹(Mocking)해서 사용했으며, 테스트 프레임워크를 직접 설치해서 사용했었지만 NestJS에서는 특정 도구를 강제하지는 않지만 JEST를 기본 테스트 프레임워크로 제공해주며 테스팅 패키지도 제공하기 때문에 개발자가 다른 도구를 찾는데 소모하는 리소스를 줄일 수 있다.

NestJS에서 제공하는 @nestjs/testing패키지를 사용하면 테스트에 사용되는 종속성만 선언해서 모듈을 만들고 해당 모듈로 UserService, UserRepository를 가져올 수 있다. 따로 단위테스트를 작성하기 위해서는 페이크 데이터를 만들어줄 필요가 있다. 기본적으로 들어갔다 나오는 결과물에 대한 것들을 지정해줘야 실제 테스팅이 이루어지기 때문이다. 물론 다른 프레임워크를 이용하면 데이터를 실제로 만들지 않아도 편리하게 할 수 있다.

Jest Mocking

const userRepositorySaveSpy = jest
		.SpyOn(userRepository, 'save')
		.mockResolvedValue(savedUser);

Jest에서는 모킹(Mocking)함수를 기본적으로 제공한다. 모킹은 단위테스트를 작성할 때. 해당 코드가 의존하는 부분을 가짜로 대체해서 만드는 것을 의미한다. 일반적으로는 테스트하려는 코드가 의존하는 부분을 직접 생성하기가 너무 부담스러울때 모킹이 사용된다. 위 코드에서는 SpyOn으로 userRepositorysave함수를 호출하는 것을 Mock하고, 이 Mock된 함수는 mockResolvedValue의 결괏값을 리턴하도록 하고 있다.

쉽게 말하면, userReposity에서 'save'를 call하는 것을 추적하면서, 결과값이 savedUser가 되도록하는 mocking함수라고 보면 된다.

이런식으로 Mocking함수에 넣어줄 실제 형태를 만들어서 삽입할 수 있다.

const savedUser: User = { firstName : 'brian', lastName: 'Kim' };

NestJS에서 단위 테스트 작성

서비스에서 사용된 updateUser 메서드

async updateUser(
  id: number,
  requestDto: UserUpdateRequestDto,
): Promise<User> {
  	const userToUpdate = await this.userRepository.findOne({where: { id: id },});

	if(!userToUpdate) {
		throw new BadRequestException(Message.NOT_FOUND_ERROR);
    }
	
	const { firstName, lastName, isActive);
    user.update(firstName, lastName, isActive);

	return this.userRepository.save(user);
}

유저 정보 수정 / 존재하지 않는 유저 정보 수정

Jest를 이용해서 위의 User 정보를 수정하는 서비스의 단위 테스트를 작성해보도록 하자!
UserServiceupdateUser 메서드를 테스트하려고하는데, 이 메서드에서는 두 가지를 테스트해야 한다. 유저 정보가 존재할 때는 성공적으로 수정하고 유저 정보가 존재하지 않을 경우엔 실패하는 로직에 대해서 검증이 필요하다.

describe('UserService', () => {
	describe('유저 정보 수정', () => {
    	if('존재하지 않는 유저 정보를 수정할 경우 NotFoundError 발생시킨다.', async () => {
        	const userId: number = 1;
            
                const updateUserDto: UpdateUserDto = {
              		firstName: 'John',
              		lastName: 'Park'
            }
                
                const userRepositoryFindOneSpy = jest
                	.spyOn(userRepository, 'findOne')
                	.mockResolvedValue(undefined);
          	
          try {
            	await userService.updateUser(userId, updateUserDto);
          } catch(e) {
            	expect(e).toBeInstanceOf(NotFoundException);
            	expect(e.message).toBe(Message.NOT_FOUND_ERROR);
          }
          
          expect(userRepositoryFindOneSpy).toHaveBeenCalledWith({
            	where: {
                  	id: userId,
                },
          });
        });
    });
})

위의 단위 테스트 코드에서는 존재하지 않는 유저의 정보를 수정할 때는 findOne 메서드가 null값을 반환할 거라고 모킹해준다. updateUser 메서드는 가짜로 null의 값이 반환되는 줄 알고 null일 때, NotFoundError가 발생하는 로직을 실행하게 된다.
expect(e).toBeInstanceOf(NotFoundException)는 이 오류 메시지 객체가 NotFoundException 클래스의 인스턴스인지 확인하는 작업이며, .toBe로 값을 비교해서 올바르게 오류 메시지가 나왔는지 검증할 수 있다.

유저 정보 수정 / 유저 정보를 성공적으로 수정

describe('UserService', () => {
  describe('유저 정보 수정', () => {
    it('유저 정보를 성공적으로 수정한다.', async () => {
      const userId: number = 1;

      const updateUserDto: UpdateUserDto = {
        firstName: 'John', 
        lastName: 'Park'
        isActive: false,
      };

      const existingUser = User.of({
        id: userId,
        firstName: 'Brian',
        lastName: 'Kim',
        isActive: true,
      });

      const savedUser = User.of({
        id: userId,
        ...updateUserDto,
      });

      const userRepositoryFindOneSpy = jest
        .spyOn(userRepository, 'findOne')
        .mockResolvedValue(existingUser);

      const userRepositorySaveSpy = jest
        .spyOn(userRepository, 'save')
        .mockResolvedValue(savedUser);

      const result = await userService.updateUser(userId, updateUserDto);

      expect(userRepositoryFindOneSpy).toHaveBeenCalledWith({
        where: {
          id: userId,
        },
      });
      expect(userRepositorySaveSpy).toHaveBeenCalledWith(savedUser);
      expect(result).toEqual(savedUser);
    });
  });
})

updateUser메서드에서 유저 정보를 찾아서 유저 정보를 정상적으로 수정했다는 로직의 테스트다. 미리 정의해놓은 existingUser을 반환할 거라고 모킹 해주고, 이 반환된 값을 수정해서 저장하면 saveduser을 반환할거라고 Mock했다. 그리고 오류가 없이 정상적으로 처리된 내용을 result에 값을 담고, Jest의 expect를 통해 검증한다. 먼저, .toHaveBeenCalledWith는 Mocking 함수가 특정 인수로 호출되었는지 확인하는데 사용될 수 있고, .toEqual로 개체의 모든 속성을 재귀적으로 비교한다.

UserServiceUser정보를 수정하는 코드의 일부분을 살펴보았다.

마치며

E2E테스트는 (End to End) 종단테스트로 환경에 의존하지만, 단위 테스트는 기본적으로 환경이랑 상관없이 빠르게 실행되어야 한다. E2@테스트에서는 보통 테스트 데이터베이스를 사용하고, 테스트의 신뢰성이 높지만 속도가 느리다는 단점이 있다.

profile
만물에 관심이 많은 잡학지식사전이자, 새로운 도전을 꿈꾸는 주니어 개발자 / 잡학지식에서 벗어나서 전문성을 가진 엔지니어로 거듭나자!
post-custom-banner

0개의 댓글