본문 바로가기
Node.js

[NestJS] Jest 단위 테스트 Mock

by WhoamixZerOne 2023. 7. 22.

✔ 필요한 종속성 설치

$ npm i --save-dev @nestjs/testing

✔ Mock Repository

Repository를 Mocking 하는 이유는 서비스 계층에서 비즈니스 로직을 검증해야 하는데 Repository를 의존하고 있다.

그래서 의존하는 것에 따라 테스트가 실패할 수도 있고 성공할 수도 있다.

그러므로 비즈니스 로직에 실패하거나 성공하는 게 아닌 다른 요소로 인해 실패할 수도 있기 때문에 의존하는 것들을

가짜로 Mock으로 만들어서 의존성을 배제하여 비즈니스 로직만 테스트하게 만든다.

// users/users.service.ts
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { Users } from './entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { UserDto } from './dto/user.dto';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(Users)
    private usersRepository: Repository<Users>,
  ) {}

  async findByEmail(email: string): Promise<Users | null> {
    return await this.usersRepository.findOne({
      where: { email },
      select: ['id', 'email', 'password'],
    });
  }

  ...

  async findById(id: number): Promise<UserDto | null> {
    return await this.usersRepository.findOne({
      where: { id },
      select: ['id', 'email', 'name', 'cellPhone', 'role'],
    });
  }
}

 

위의 서비스가 있고 서비스를 생성할 때 만들어지는 "users.service.spec.ts" 파일이 초기의 상태 그대로일 때

"run npm test" 명령어를 실행하면 한 번 봤을 만한 에러가 나타나게 된다.

 

Nest can't resolve depedencies of the UsersService (?). Please make sure that the argument UsersRepository at index [0] is available in the RootTestModule context.

 

등의 여러 에러 문구를 볼 수 있다. 즉, 생성자에 index [0]에서 Repository 의존성 주입을 하고 있으니 그걸 만들어 달라는 얘기이다.

// users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { Repository } from 'typeorm';
import { Users } from './entities/user.entity';
import { getRepositoryToken } from '@nestjs/typeorm';

type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>;

const mockUsersRepository = () => ({
  create: jest.fn(),
  save: jest.fn(),
  findOne: jest.fn(),
});

describe('UsersService', () => {
  let service: UsersService;
  let usersRepository: MockRepository<Users>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: getRepositoryToken(Users),
          useValue: mockUsersRepository(),
        },
      ],
    }).compile();

    service = module.get<UsersService>(UsersService);
    usersRepository = module.get<MockRepository<Users>>(
      getRepositoryToken(Users),
    );
  });

  it('userService 서비스 존재', () => {
    expect(service).toBeDefined();
  });
  
  ...
});

여기서 중요한 점들은 모듈의 providers에 Repository의 Mock를 등록하는 점과 Repository에서 사용하는 함수를 Mocking 하는 점이다.

 

const module: TestingModule = await Test.createTestingModule({
  providers: [
    UsersService,
    {
      provide: getRepositoryToken(Users),
      useValue: mockUsersRepository(),
    },
  ],
}).compile();

이 부분은 @Module 데코레이터에 등록하는 것과 동일하다고 보면 된다.

provide에 주어진 Entity기반으로 준비된 토큰을 반환하는 getRepositoryToken()을 사용해서 등록한다.

그리고 Repository를 대체할 MockRepository로 mockUsersRepository 변수에 함수로 만들어 놨고,

useValue로 등록했다.

 

다른 방법으로 Class에 Mock 메서드로 만들어서 useClass로 등록하는 방법도 있다.

 

NestJS 공식문서 참고 자료

 

type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>;

const mockUsersRepository = () => ({
  create: jest.fn(),
  save: jest.fn(),
  findOne: jest.fn(),
});

describe('UsersService', () => {  
  ...
  let usersRepository: MockRepository<Users>;

  beforeEach(async () => {
    ...    
    usersRepository = module.get<MockRepository<Users>>(
      getRepositoryToken(Users),
    );
  });
  ...
});

"MockRepository" 타입을 만들고 usersRepository 변수에 타입을 MockRepository<Entity> 지정한다.

마지막으로 module.get<MockRepository<Entity>>(getRepositoryToken(Entity))로 모듈에 등록한 Entity Repository를 가져온다.

더보기

MockRepository는 Repository<Entity>를 Mocking 하기 위해 타입으로 만든다.

 

1. Record<K, T>

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

K는 key 값의 타입으로, T는 값의 타입으로 갖는 타입을 리턴한다. 즉, Repository에 있는 필드들을 jest.Mock 값으로 갖게 된다는 것이다.

create: jest.fn(), save: jest.fn(), find: jest.fn(), findOne: jest.fn() 등으로 사용할 수 있다.

 

2. Partial<T>

type Partial<T> = {
    [P in keyof T]?: T[P];
};

T에 대한 모든 속성(프로퍼티)들을 Optional(?)하게 변경한다. 즉, Repository에 있는 필드들을 사용하는 게 아니기 때문에 Optional 하게 변경시킨다.

create?: jest.fn(), save?: jest.fn() 등으로 사용한다.

 

그러면 이제 아래와 같이 Mocking 한 함수들을 사용할 수 있다.

it('회원가입 성공 true 반환', async () => {
  usersRepository.create.mockReturnValue(joinUser);
  usersRepository.save.mockReturnValue(saveUser);
  
  ...
});

✔ Mock Service

Service를 Mock 하는 것은 Repository를 Mock 하는 것과 다를 것이 없다. 타입만 맞춰주면 된다.

// users/users.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';

type MockService<T = any> = Partial<Record<keyof T, jest.Mock>>;

const mockUsersService = () => ({
  findByEmail: jest.fn(),
  findById: jest.fn(),
});

describe('UsersController', () => {
  let controller: UsersController;
  let usersService: MockService<UsersService>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [UsersController],
      providers: [
        {
          provide: UsersService,
          useValue: mockUsersService(),
        },
      ],
    }).compile();

    controller = module.get<UsersController>(UsersController);
    usersService = module.get<MockService<UsersService>>(UsersService);
  });

  it('should be defined', () => {
    expect(controller).toBeDefined();
  });
  
  it('인증 성공 시 유저 정보 반환', async () => {
    usersService.findByEmail.mockReturnValue(user);
    ...
  });
});

 

 

🔗 Reference

댓글