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
를 사용하여 프로바이더 구성을 자동화하는 방법을 찾았습니다.
// 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에 할당합니다.
MockRepositoryFactory
의 getMockRepository
메서드는 레포지토리 클래스 정보를 받아 해당하는 모킹 레포지토리 객체를 반환하며, 이 객체를 동적 프로바이더의 useValue로 지정하면 모킹 레포지토리를 구성할 수 있습니다.이러한 개선을 통해 테스트 코드의 유지 관리를 간소화하고, 불필요한 중복작업을 줄일 수 있게 되었습니다. 이제 테스트 환경 구성이 더욱 빠르고 간편해졌습니다. 더 나아가 테스트 자동화의 효율성을 극대화 할 수 있는 방법을 지속적으로 고민할 것입니다.