[NestJS] test 코드 직접 모킹 자동화 하기

허창원·2024년 4월 26일
0
post-custom-banner

유닛 테스트 시 직접 모킹 코드의 문제점

  const mockUserRepository = {
    findByUsername: jest.fn(),
    save: jest.fn(),
  }

describe('AuthService', () => {
  let authService: AuthService

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        AuthService,
        {
          provide: UserRepository,
          useValue: mockUserRepository
        },
      ]
    }).compile()
  })
})

유닛 테스트를 구성할 때, 테스트 대상과 모킹 대상은 분리되어있습니다. 서비스 코드를 테스트 하기 위해서는 테스트 대상인 서비스 자체와 해당 서비스에서 사용하는 하위 레포지토리들을 모킹하여 테스트 모듈에 프로바이더로 지정해야합니다. jest.mock()을 사용하여 대상 레포지토리의 모든 요소를 모킹하고, 동적 프로바이더 패턴으로 작성한 모킹 레포지토리 프로바이더를 테스트 모듈의 프로바이더로 설정함으로써 테스트를 위한 구성을 완성할 수 있습니다.

그러나 이 방법은 Repository 클래스가 기본적으로 지원하는 'save', 'insert' 등의 메서드와 모킹 대상인 메서드들을 모드 직접 모킹하여 작성해야 합니다. 이 과정에서 서비스 코드에서 레포지토리에 새로운 메서드를 추가하거나 삭제할 때마다 테스트 코드의 모킹 부분도 수정해야하는 번거로움이 발생합니다. 이 문제를 해결하기 위해서 Mock Repository Factory를 사용하여 프로바이더 구성을 자동화하는 방법을 찾았습니다.

MockRepository Factory를 이용한 프로바이더 자동화

// mock-repository.factory.ts
import { Repository } from 'typeorm'

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

export class MockRepositoryFactory {
  static getMockRepository<T>(
    type: new (...args: any[]) => T,
  ): MockRepository<T> {
    const mockRepository: MockRepository<T> = {}

    Object.getOwnPropertyNames(Repository.prototype)
      .filter((key: string) => key !== 'constructor')
      .forEach((key: string) => {
        mockRepository[key] = jest.fn()
      })

    Object.getOwnPropertyNames(type.prototype)
      .filter((key: string) => key !== 'constructor')
      .forEach((key: string) => {
        mockRepository[key] = type.prototype[key]
      })

    return mockRepository
  }
}
// budget.service.spec.ts
describe('BudgetRepository', () => {
  let studyRepository: MockRepository<BudgetRepository>;

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

    studyRepository = module.get(getRepositoryToken(BudgetRepository));
  });
});

keyof Repository<T>, jest.Mock를 통해 레포지토리가 가지고 있는 메서드를 추출하고,
Record<keyof Repository<T>, jest.Mock>를 통해 , key 타입은 아까 추출한 타입으로, value 타입은 jest.Mock으로 지정합니다.
Partial<Record<keyof Repository<T>, jest.Mock>>를 통해 정의된 메서드의 일부를 사용할 것이기 때문에 선택적으로 처리합니다.
즉, type MockRepository는 객체 형태입니다. key 타입은 Repository<T>가 가가지고 있는 타입이고, value의 타입은 jest.Mock 타입이 됩니다.

static getMockRepository<T>(type: new (...args: any[]) => T) 이 함수에 들어오는 type은 T라는 타입으로 인스턴스를 생성하는 constructor 함수를 말합니다.
const mockRepository: MockRepository<T> = {} mockRepository는 mocking한 함수들을 채울 객체입니다.
Object.getOwnPropertyNames(Repository.prototype) 생성자를 제외한 모든 속성을 Repository 클래스에서 가져옵니다.
.filter((key: string) => key !== 'constructor') .forEach((key: string) => { mockRepository[key] = jest.fn() }) 생성자가 아닌 것을 filter하여 각각의 속성에 대해서 jest.fn()으로 모의하여 mockRepository에 할당합니다.

  • 이 접근 방식은 필요한 메서드들과 테스트 대상이 되는 모든 메서드들을 일일이 나열하여 프로바이더의 useValue 객체를 구성하는 번거로움을 줄입니다. MockRepositoryFactorygetMockRepository 메서드는 레포지토리 클래스 정보를 받아 해당하는 모킹 레포지토리 객체를 반환하며, 이 객체를 동적 프로바이더의 useValue로 지정하면 모킹 레포지토리를 구성할 수 있습니다.
  • 클래스 문법으로 정의된 메서드들은 내부적으로 생성자 함수의 프로토타입 객체에 저장됩니다. 따라서 'Repository.prototype', 'type.prototype'의 프로퍼티를 참조하여 모킹 레포지토리를 구성합니다.
  • 커스텀 레포지토리에서 추가로 정의된 메서드들은 'Object.getOwnPropertyNames()'를 사용하면 확인할 수 있습니다.
  • 생성자 함수의 프로토타입 객체에는 정의된 모든 메서드들 외에도 생성자 함수를 가리키는 constructor 프로퍼티가 포함되어 있으므로 filter를 사용해 이를 제외합니다.

이러한 개선을 통해 테스트 코드의 유지 관리를 간소화하고, 불필요한 중복작업을 줄일 수 있게 되었습니다. 이제 테스트 환경 구성이 더욱 빠르고 간편해졌습니다. 더 나아가 테스트 자동화의 효율성을 극대화 할 수 있는 방법을 지속적으로 고민할 것입니다.

post-custom-banner

0개의 댓글