실제 DB를 연결해 Service Layer 를 테스트하는 방법이 궁금하신 분들은 이 글을 참고해 주세요.
🐳문제상황
Service Layer를 테스트하기 위해서는 Repository를 Mocking 하거나 실제 Repository를 사용하거나 하는 방법을 사용해야 한다.
이 글에서는 NestJS에서 제공하는 TestingModule 기능을 활용해 Repository 레이어를 Mocking해 테스트코드를 작성하는 방법을 설명하려고 한다.
🐳 TestingModule을 사용하여 mocking하기
jest 에서 제공하는 기능들 jest.fn , jest.spyOn 등을 이용하여 mocking 한 후에 테스트코드를 작성하는 것도 가능하지만 테스트코드가 길어져 가독성이 떨어지는 것 같아. 다른 방법을 찾아보았다.
첫 번째 방법은 NestJS에서 제공하는 TestingModule 기능을 활용하여 Repository를 mocking하는 것이다. UserService 테스트하는 코드를 이용하여 예시와 함께 살펴보자.
순서는 아래와 같다.
- UserRepository와 같은 기능을 하는 MockUserRepository 를 구현해 준다.
- TestingModule을 이용하여 UserService 에 주입돼야 할 UserRepository 대신에 MockUserRepository를 주입해 준다.
- 테스트코드를 작성한다.
1. 먼저 TypeORM의 UserRepository기능을 대신할 MockUserRepository 코드를 구현해 보자
export class MockUserRepository {
private users: User[] = [];
public async save(createAuthDto: CreateAuthDto) {
const { password, ...result } = createAuthDto;
return result;
}
public async findOne(options) {
const property = Object.keys(options.where)[0];
const user: User = this.users.find(
(user) => user[property] === options.where[property],
);
if (!user) {
throw new NotFoundException('해당 ID의 유저가 없습니다.');
}
const { password, ...result } = user;
return result;
}
public async findAndCount(options) {
const list = this.users.slice(options.skip, options.skip + options.take);
const count = this.users.length;
return [list, count];
}
}
1) 먼저 실제 데이터베이스를 사용할 것은 아니기 때문에 User 정보를 저장할 users 배열을 만들어 주었다.
외부에서 접근은 불가능해야 하기 때문에 private으로 설정해 주자!
private users: User[] = [];
2) 다음은 TypeORM repository 에서 제공하는 save 메소드를 구현한 코드이다.
id 값을 생성해 준 후에 user 를 위에서 만든 임시 DB에 저장한다.
그다음 user 객체를 그대로 리턴한다.
public async save(user: User) {
// const { password, ...result } = createAuthDto;
user.id = 'TEST_' + this.users.length + 1;
this.users.push(user);
return user;
}
3) 다음은 TypeORM 에서 제공하는 findOne 과 findAndCount 메소드를 구현한 것이다.
실제로 DB에 접속하는 것이 아니라 위에서 만든 users 배열에서 검색 조건으로 입력한 값들을 찾는 것이다.
이렇게 작성한 코드는 어떤 의미가 있는지 아래의 테스트코드를 보면서 함께 살펴보자
2. TestingModule 생성하기
describe('AuthService Test', function () {
let service: AuthService;
let userRepository: MockUserRepository;
let jwtService: MockJwtService;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{
provide: getRepositoryToken(User),
useClass: MockUserRepository,
},
{
provide: JwtService,
useClass: MockJwtService,
},
],
}).compile();
service = module.get<AuthService>(AuthService);
userRepository = module.get<MockUserRepository>(getRepositoryToken(User));
jwtService = module.get<MockJwtService>(JwtService);
});
it('authService should be defined', function () {
expect(service).toBeDefined();
expect(service.validateUser).toBeDefined();
expect(service.signup).toBeDefined();
});
});
이 코드에서 beforeAll 부분에 모든 테스트코드를 실행하기 전에 TestingModule을 생성해 주자.
이때 provider 부분을 잘 살펴보자 !
먼저 테스트할 Service인 AuthService 를 주입해 주고 있다.
이 AuthService는 userRepository와 JwtService 를 주입받고 있기 때문에 이 의존성을 해결해 줘야 한다.
그래서 아래 부분의 코드가 등장한다.
{
provide: getRepositoryToken(User),
useClass: MockUserRepository,
},
{
provide: JwtService,
useClass: MockJwtService,
},
userRepository와 JwtService를 모두 직접 작성한 Mocking Class로 대체하기 위한 코드이다.
provider에는 주입하려는 Class 명을 입력해 준다.
그리고 그 아래에는 useClass를 이용해서 대체할 Class 명을 입력해 준다. ( useValue , useExisting 등 다양한 방법이 존재한다. )
위의 방법으로 의존성을 해결한 후에 'authService should be defined' 테스트를 통해 제대로 service 가 생성되었는지 확인해 보자 !
의존성 문제없이 service가 제대로 생성된 것을 확인할 수 있다 !
3. 테스트코드를 작성하자.
it('signup return User without password', async () => {
const dto = new CreateAuthDto();
dto.username = 'testID';
dto.password = 'testpwd123!';
dto.name = 'testname';
dto.mobileNumber = '010-8098-1398';
dto.birthday = '931117';
dto.email = 'sksk8922@gmail.com';
const signupResult = await service.signup(dto);
expect(signupResult.username).toBe(dto.username);
expect(signupResult.name).toBe(dto.name);
expect(signupResult.mobileNumber).toBe(dto.mobileNumber);
expect(signupResult.birthday).toBe(dto.birthday);
expect(signupResult.email).toBe(dto.email);
expect(signupResult.password).not.toBeDefined();
});
createAuthDto를 signup method에 전달했을 때의 결과를 테스트하는 코드이다.
( 번외 : 이때 dto를 생성하는 코드가 길기 때문에 가독성이 떨어진다. 이럴 때는 별도의 함수로 분리하여 간결하게 코드를 작성하는 것도 좋다 )
이 singup 메소드는 내부에서 userRepository.save 함수를 호출한다.
이때 우리가 작성한 위의 MockUserRepository의 save 메소드가 사용되게 된다.
그런데 우리는 MockingUserRepository를 사용하고 있으므로 실제 DB를 연결하지 않았음에도 마치 TypeORM을 사용하고 있는 것처럼 테스트가 가능하다.
그리고 테스트코드 내부에도 jest.spyOn 같은 다른 mocking 함수가 사용되고 있지 않다.
MockUserRepository를 구현해야 한다는 단점이 있지만 테스트코드가 간결해지는 모습을 확인할 수 있다 !
실행결과도 살펴보자.
아주 깔끔하게 성공한 것을 볼 수 있다.
signup 내부에서 userRepository.save 함수를 MockUserRepository를 이용해서 잘 해결해 준 것을 확인할 수 있다.
🐳 결론
위의 방법처럼 MockClass를 구현하면 코드가 mocking 없이 실제로 동작하는 것처럼 테스트코드를 구현할 수 있다.
그리고 jest.fn 이나 jest.spyOn 등을 사용하지 않기 때문에 코드가 간결해지는 장점이 있다.
하지만 복잡한 기능을 테스트해야 할수록 MockClass를 구현하는데 많은 시간과 에너지가 소요된다.
또한 MockClass를 잘못 구현한 경우 잘못된 결과가 나왔음에도 테스트가 통과되는 결과가 생길 수 있기 때문에 조심해야 할 것이다.
하지만 간단한 기능을 테스트하고 싶을 때는 나쁜 방법이 아닐 것 같다 !
'테스트 코드' 카테고리의 다른 글
[NestJS] 실제 DB와 연결해 Service Layer 테스트하기 + runInBand (0) | 2024.03.03 |
---|---|
[TDD] 테스트 코드가 필요한 이유? (0) | 2023.11.12 |
ts-mockito 와 deepEqual 사용하기 (0) | 2023.07.08 |