Nestjs 유닛 테스트 AutoMocking

박진(TsTunas)·2023년 2월 28일
0

AutoMocking이 필요한 이유

이러한 컨트롤러를 테스트한다고 생각해보자.

// auth.controller.ts
import { Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';

@Controller('api/auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('sample')
  async createSampleUser() {
    return await this.authService.createSampleUser();
  }
}

이 컨트롤러가 의존하는 서비스는 아래 코드이다.

// auth.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';

@Injectable()
export class AuthService {
  constructor(@InjectRepository(User) private usersRepository: Repository<User>) {}

  async createSampleUser() {
    let arr = [];
    for (let i = 1; i <= 5; i++) {
      const temp = {
        email: `test_emai_${i}@gmail.com`,
        password: 'qwer1234',
      };
      arr.push(temp);
    }
    return await this.usersRepository.insert(arr);
  }
}

TypeOrm의 리포지토리를 의존성으로 가지는 서비스라는 걸 알 수 있다.

다음엔 컨트롤러 파일을 nest cli로 만들 때에 같이 만들어진 테스트 파일을 보자.

// auth.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { TestService } from './test.service';

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

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

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

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

이렇게 시작하면 바로 에러가 날 것이다. 왜냐하면 테스트로 만들어진 모듈에서 AuthService를 provide하지 않기 때문이다. 물론 provider에 AuthService를 추가해도 에러가 나는데, 이번엔 AuthService가 필요한 UserRepository 주입이 안 되기 때문이다.

여기서 필요한 게 실제 사용되는 객체를 가짜(Mock)로 바꾸어 사용하는 Mocking이다. 여러가지 방법이 있다.

  1. provider에 token은 실제 클래스로 하고 제공하는 클래스는 가짜로 하는 방법.
{
	provide: AuthService,
    useClass: MockAuthService
}

이 방법은 안정적이다. 가짜를 제공하기에 의존성 문제도 없고 실제 클래스처럼 만들 수 있다.

  1. spy를 사용하는 방법
// service는 module.get(AuthService)로 가져왔다고 가정한다.
const createSampleUserSpy = jest.spyOn(service, 'createSampleUser');
createSampleUserSpy.mockResolvedValue('spy')

const result = await service.createSampleUser() // spy

spy를 사용하면 실제 객체의 메소드가 실행되는 걸 가로채서 자기가 실행된다. 이 경우엔 실제 객체가 주입된다. 다만 해당하는 메소드가 가짜 함수로 실행될 뿐이다.

오토 모킹

위의 두 방법을 사용해도 괜찮겠지만, 나는 Nestjs 공식문서에서 알려준 AutoMocking이 마음에 들었다.
https://docs.nestjs.com/fundamentals/testing#auto-mocking

해당 페이지의 예시코드를 보자.

import { ModuleMocker, MockFunctionMetadata } from 'jest-mock';

const moduleMocker = new ModuleMocker(global);

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

  beforeEach(async () => {
    const moduleRef = await Test.createTestingModule({
      controllers: [CatsController],
    })
    .useMocker((token) => {
      const results = ['test1', 'test2'];
      if (token === CatsService) {
        return { findAll: jest.fn().mockResolvedValue(results) };
      }
      if (typeof token === 'function') {
        const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
        const Mock = moduleMocker.generateFromMetadata(mockMetadata);
        return new Mock();
      }
    })
    .compile();
    
    controller = moduleRef.get(CatsController);
  });
});

컨트롤러를 테스트하는데 provider가 하나도 없다. 그럼 CatController의 의존성은 어떻게 처리하는 걸까. 답은 바로 useMocker로 token 별로 주입하는 함수를 return한다. 내가 이해한 바로는 위에서는 CatsService를 토큰으로 하는 의존성 주입은 메소드가 jest.fn()인 걸 반환하도록 되어있다.
그 다음에 typeof token === 'function'은 함수(클래스 포함)가 토큰인 경우에 자동으로 mocking화 시키는 거 같다. 그냥 원래 주입하려는 것을 모킹해서 가짜로 주입한다, 정도로만 이해해도 될 거 같다.
token 자체가 함수라면 제공하는 가짜함수를 그 함수 자체를 이용해서 만드는 느낌이다.

그럼 TypeOrm의 리포지토리는 어떻게 할까.

TypeOrm에서 제공하는 함수를 보면 getRepositoryToken이라는 함수가 있다. 엔티티를 인자로 받으면 그 엔티티로 제공하는 리포지토리의 토큰을 알 수 있다. 즉, @InjectRepository(entity)가 반환하는 @Inject(token)를 생각했을 때 그 token을 알 수 있으므로, useMocker에서 token이 getRepositoryToken(entity)일 때, Mock을 반환해주면 된다.

	.useMocker((token) => {
        if (token === getRepositoryToken(User)) {
          return {
            findOneBy: jest.fn(),
            ...
          }
        }
      })

마지막으로

useMocker 자체가 autoMocking은 아니다. providers에 Mock를 제공하도록 적었던 수동 mocking 방법을 다르게 쓴 것으로 보인다.
autoMocking이란 ModuleMocker로 import한 모든 모듈(다른 파일에서 내보낸 것)을 mocking하고 토큰이 함수일 때, 그 만들어낸 mocking을 주입하는 것이라고 보인다.

profile
자바스크립트 전문가가 되고 싶은 아마추어

0개의 댓글