Node, Nest, Deno/🦁 Nest - Series

Nest + Jest unit test (2) Mocking

DarrenKwonDev 2020. 11. 27. 21:40

테스트 환경 조성하기(Module과 같게)

 

테스트를 하기 위해서는 코드가 실행되는 환경과 같은 환경을 만들어줘야 합니다. 

여기서는 Service를 테스트하기 위해 각 Module과 동일한 형태의 환경을 만들어 줄 것입니다.

 

예를 들면 이렇습니다.

 

실제 로직을 가진 jwtModule은 아래와 같은 provider를 가지고, 해당 provider를 service에서 사용합니다.

@Module({})
@Global()
export class JwtModule {
  static forRoot(options: JWTModuleOptions): DynamicModule {
    return {
      ...options,
      global: true,
      module: JwtModule,
      providers: [
        {
          provide: CONFIG_OPTIONS,
          useValue: options,
        },
        {
          provide: JwtService,
          useClass: JwtService,
        },
      ],
      exports: [JwtService],
    };
  }
}

 

따라서 Test에서도 해당 모듈이 사용하는 provider 가져와 사용할 수 있는 환경을 만들어주었습니다.

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

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        JwtService,
        {
          provide: CONFIG_OPTIONS,
          useValue: { privateKey: TEST_KEY },
        },
      ],
    }).compile();

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

 

 

Mocking

 

아래와 같은 Service를 테스트하고 싶다고 가정해봅시다.

User, Verification과 같은 Repo들을 Inject해줘야하고, 다른 Service도 불러와야 합니다.

export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly users: Repository<User>,
    @InjectRepository(Verification)
    private readonly verification: Repository<Verification>,
    private readonly config: ConfigService,
    private readonly jwtService: JwtService,
    private readonly mailService: MailService,
  ) {}

  async createAccount({ email, password, role }: CreateAccountInput): Promise<{ ok: boolean; error?: string }> {
    try {
      ... 중략

 

그런데 실제로 동일한 환경을 구성해준다면 실제 DB에 dummy 데이터가 들어가게 될 것이고, 나중에 이를 cleanup해줘야 하는 수고를 해야 합니다. 그 과정에서 DB를 망가뜨릴 수 있는 실수할 가능성도 존재합니다.

 

여러모로 이러한 불편한 점 때문에 typeORM에서는 테스트를 위한 mocking DB를 구성할 수 있습니다.

DB 뿐만 아니라 service 또한 직접 불러오는 것이 아닌 존재하는 것처럼 작성할 수 있는데

이를 Mocking(흉내내기) 라고 합니다.

 

테스트하고자 하는 UserService에서 User Repo에서 사용하는 메서드는 findOne, save, create가 있습니다.

typeORM에서 getRepositoryToken이라는 메서드를 제공하니 이를 활용하여 아래와 같이 작성하는 방식으로 mocking을 할 수 있습니다.

 

그런데 이 방식은 조금 틀렸습니다.

// mocking
const mockRepository = {
  findOne: jest.fn(),
  save: jest.fn(),
  create: jest.fn(),
};

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

  beforeAll(async () => {
    const modules = await Test.createTestingModule({
      providers: [
        UsersService,
        { provide: getRepositoryToken(User), useValue: mockRepository }, // Mocking user repo
      ],
    }).compile();
    service = modules.get<UsersService>(UsersService);
  });

 

mockRepository로 만든, fake repo들을 다른 typeorm의 entity들이 공유해서는 안되기 때문에 함수 형태로 작성하는 것이 좋습니다. 아래와 같이 말이죠.

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

describe('UserService', () => {
  let service: UsersService;
  let userRepository: MockRepository<User>;

  beforeAll(async () => {
    const modules = await Test.createTestingModule({
      providers: [
        UsersService,
        { provide: getRepositoryToken(User), useValue: mockRepository() },
        { provide: getRepositoryToken(Verification), useValue: mockRepository() },
      ],
    }).compile();
    service = modules.get<UsersService>(UsersService);
    userRepository = modules.get(getRepositoryToken(User));
  });

 

한편 entity Repo가 아닌 다른 Service들 또한 mocking할 수 있습니다. 사용하는 메서드들을 가져온 후 provider로 제공해줍시다. 아래와 같이요. 여기서 mockJwtService는 다른 곳에서 중복되어 사용되지 않으니 함수형태로 만들지는 않았습니다.

// 일반 Service mocking
const mockJwtService = {
  sign: jest.fn(),
  verify: jest.fn(),
};

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

  beforeAll(async () => {
    const modules = await Test.createTestingModule({
      providers: [
        UsersService,
        { provide: JwtService, useValue: mockJwtService }, // 일반 service 모킹
      ],
    }).compile();
    service = modules.get<UsersService>(UsersService);
  });

 

 

최종적인 mocking은 아래와 같습니다.

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

const mockJwtService = {
  sign: jest.fn(),
  verify: jest.fn(),
};

const mockMailService = {
  sendVerificationEmail: jest.fn(),
};

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

  beforeAll(async () => {
    const modules = await Test.createTestingModule({
      providers: [
        UsersService,
        { provide: getRepositoryToken(User), useValue: mockRepository(),
        { provide: getRepositoryToken(Verification), useValue: mockRepository()},
        { provide: JwtService, useValue: mockJwtService },
        { provide: MailService, useValue: mockMailService },
      ],
    }).compile();
    service = modules.get<UsersService>(UsersService);
  });

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

 

 

Utility Type을 활용한 Mock Type

 

추가적으로, mocking한 repo나 서비스를 이용하여 테스트 코드를 작성하여야 합니다.

실제로 불러온 repo나 서비스가 아니기 때문에 메서드를 타이핑해주는 과정이 조금 다릅니다.

 

우선 다음과 같이 Repo 타이핑을 진행할 수 있습니다.

여기서 사용된 utility type은 핸드북 이나 제 포스트를 참고하시면 편합니다.

 

keyof Repository<T>로 해당 레포지토리가 가지고 있는 메서드를 추출한 다음,

해당 타입을 Key값 타입으로, jest.Mock 타입을 밸류 값 타입으로 갖는 타입을 리턴합니다.

그리고 정의된 메서드를 전부 사용하는 것이 아닌 일부만 사용하기 때문에 Partial로 감싸 optional 처리를 진행해줍니다.

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

// mocking repo의 타이핑
let userRepository: MockRepository<User>;

 

실제 활용은 아래와 같습니다.

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

describe('UserService', () => {
  let service: UsersService;
  
  // mocking repo의 타이핑
  let userRepository: MockRepository<User>;

  beforeAll(async () => {
    const modules = await Test.createTestingModule({
      providers: [
        UsersService,
        { provide: getRepositoryToken(User), useValue: mockRepository },
        { provide: getRepositoryToken(Verification), useValue: mockRepository },
        { provide: JwtService, useValue: mockJwtService },
        { provide: MailService, useValue: mockMailService },
      ],
    }).compile();
    
    service = modules.get<UsersService>(UsersService);
    
    // 사용할 mocking repo
    userRepository = modules.get(getRepositoryToken(User));
  });

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

  describe('createAccount', () => {
    it('should fail if user exists', () => {
      .. 중략
    });
  });