Nestjs 컨트롤러 단위 테스트를 해보고 싶었다!
// user.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { CreateUserReqDto } from './dtos/create.dto';
import { UserService } from './user.service';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post('/')
public async create(@Body() createReqDto: CreateUserReqDto) {
return await this.userService.create(createReqDto);
}
}
// user.service.ts
import { Inject, Injectable } from '@nestjs/common';
import { CreateUserReqDto, CreateUserResDto } from './dtos/create.dto';
import { IUserRepository } from './interfaces/user.repository.interface';
import { DbInjectionToken } from '../../libs/db/db.injection.token';
@Injectable()
export class UserService {
constructor(@Inject(DbInjectionToken.USER_REPOSITORY) private readonly userRepository: IUserRepository) {}
public async create(createReqDto: CreateUserReqDto): Promise<CreateUserResDto> {
return new CreateUserResDto(await this.userRepository.create(createReqDto));
}
}
UserContoroller는 userService를 생성자 주입 받고 있고,
UserService는 userRepository를 생성자 주입 받고 있다.
// user.controller.spec.ts
describe('UserController', () => {
let controller: UserController;
let userService: UserService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UserController],
providers: [UserService],
}).compile();
controller = module.get<UserController>(UserController);
userService = module.get<UserService>(UserService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

controller 테스트를 위해서 Test Module에 UserService 만을 주입하면 이미지와 같은 에러가 발생한다.
DBInjectionToken에 정의되어 있는 USER_REPOSITORY를 찾지 못하는 것 같아 UserProvider를 제공해주었다!
const module: TestingModule = await Test.createTestingModule({
controllers: [UserController],
providers: [UserService, ...UserProviders],
}).compile();

이번엔 Token에 사용되는 Class UserRepository를 찾지 못하는 에러가 발생한다!
const module: TestingModule = await Test.createTestingModule({
imports: [DbModule],
controllers: [UserController],
providers: [UserService, ...UserProviders],
}).compile();

TypeOrmModule에 forRootAsync로 DBConfigProvider를 사용하고 있는데 이 때 환경 변수를 가져오기 위해 EnvService를 사용하고 있다.
그렇기 떄문에 DbModule을 TestCode에 주입하게 되면 환경 변수 관련된 서비스를 찾지 못한다는 에러가 발생하게 되는 것이다.
🤔 Controller의 역할을 생각해보면 요청받은 HTTP를 Service에 전달하고 Service에서 처리된 요청 결과를 반환해주는 것이다.
이 때 DB가 잘 연결되는 지의 테스트까지 필요할까? 라는 의문이 들었다.
그렇기에 Mock처리를 하는 것 아닐까?
const mockUserRepository = {
crete: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
controllers: [UserController],
providers: [
UserService,
...UserProviders,
{
provide: getRepositoryToken(UserEntity),
useValue: mockUserRepository,
},
],
}).compile();
getRepositoryToken을 사용하면 Service에 필요한 Repository를 모의로 핸들링할 수 있게 된다!
When it comes to unit testing an application, we usually want to avoid making a database connection, keeping our test suites independent and their execution process as fast as possible. But our classes might depend on repositories that are pulled from the data source (connection) instance. How do we handle that? The solution is to create mock repositories. In order to achieve that, we set up custom providers. Each registered repository is automatically represented by an Repository token, where EntityName is the name of your entity class.
getRepositoryToken은 모의 리포지토리를 만들어 주는 함수이다.
단위 테스트 시 데이터베이스의 연결 하지 않고 단위 테스트를 작성하고 싶을 때 Nestjs가 제공하는 getRepositoryToken을 활용하면 된다.
getRepositoryToken의 매개 변수로 전달된 Entity에 useValue로 정의된 모의 레포지토리가 자동으로 매핑되는 것이다!
이제 대체 mockUserRepository가 UsersRepository로 사용된다. 어떤 클래스에서든 @InjectRepository() 데코레이터를 사용하여 UsersRepository를 요청할 때마다 Nest는 등록된 mockRepository 객체를 사용하는 것이다!!