개발환경
- Nest.js
- Jest
(+) ts-mockito
(+) typeorm-transactional-cls-hooked
Jest에 기본으로 있는 Mock을 사용하면서 다양한 불편함(문자열로 메소드 주입하기, 모킹하는 과정이 구구절절 뭔가 많음...) 을 느꼈는데 조졸두님의 글을 보고 ts-mockito를 사용해보기로 했다. 자세한 사용법은 해당 라이브러리 공식 깃허브에서 확인할 수 있다.
테스트할 User Service
import { InjectRepository } from '@nestjs/typeorm';
import { Injectable } from '@nestjs/common';
import { Transactional } from 'typeorm-transactional-cls-hooked';
// ...
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private readonly userRepository: UserRepository,
) {}
@Transactional()
async saveUser(user: User): Promise<User> {
return await this.userRepository.save(user);
}
async findUser(id: number): Promise<User> {
return await this.userRepository.findById(id);
}
}
트랜잭션 처리를 위해 typeorm-transactional-cls-hooked 라이브러리를 사용했다. 추천해준 사수에게 리스펙😏
User 도메인
@Entity({ name: 'customer' })
export class Customer {
@PrimaryColumn({ name: 'customer_id' })
id?: number;
@Column()
name: string;
constructor(params?: { id?: number; name?: string }) {
if (params) {
this.id = params.id;
this.name = params.name;
}
}
}
첫 테스트 코드는 다음과 같이 짰다.
describe('UserService', () => {
let service: UserService;
let userRepository: UserRepository;
// 내 경우 TypeORM에 종속적이지 않은 repo를 만들기 위해
// UserRepository를 인터페이스로 만들고 mock할 때 구현체를 주입했다.
beforeEach(async () => {
userRepository = mock(TypeormUserRepository);
when(userRepository.saveUser(anything()))
.thenResolve(createUser({}))
when(userRepository.findById(anyNumber())).thenResolve(createUser({}));
when(userRepository.findAll()).thenResolve(users);
let instanceUserRepo = instance(userRepository);
service = new UserService(instanceUserRepo);
});
afterEach(async () => {
reset(userRepository);
});
it('should be defined', () => {
expect(service).toBeDefined();
expect(userRepository).toBeDefined();
});
it('saveUser', async () => {
const input = createUser({});
const user = await service.saveUser(input);
expect(user.name).toBe(input.name);
expect(user.id).toBe(input.id);
});
it('findUsers', async () => {
const users = await service.findUsers();
expect(users.length).toBe(2);
});
});
const createUser = (params: Partial<User>) =>
new User({
id: params.id || 1234,
name: params.name || '춘식',
});
const users = [
createUser({}),
createUser({ id: 5678, name: '스카피' }),
];
결과는?
- saveUser 실패
- findUser 성공
saveUser의 경우 트랜잭션이 걸려있어, No CLS namespace defined in your app ... 에러가 떴다.
적혀있는 대로 initializeTrasactionalContext();를 적으면 아래와 같이 Connection "default" was not found 에러가 뜬다.
해당 에러에 대해 서치하면서 스택오버플로우 QnA와 TypeORM 이슈를 모두 살펴봤지만 둘 다 큰 도움은 되지 않았다.
디버깅하면서 확인해보니 typeorm-transactional-cls-hooked의 Transactional 데코레이터를 인지하면 테스트 모듈 설정을 받는데... 도중에 undefined가 입력되면서 뜨는 에러였다. 그럼 이 부분을 모킹하면 해결될까? 해서 찾아보았다. 그리고...
https://github.com/odavid/typeorm-transactional-cls-hooked/issues/2
위 링크에서 해답을 찾을 수 있었다!!!! 😊
아래처럼 describe 전에 Trasactional 을 모킹하면 해결되는 문제였다. (간략하게 썼지만 이걸 해결하기 위해 이틀을 날렸다...🥲)
// imports
...
jest.mock('typeorm-transactional-cls-hooked', () => ({
Transactional: () => () => ({}),
BaseRepository: class {},
IsolationLevel: { SERIALIZABLE: 'SERIALIZABLE' },
// 내 경우 BaseRepository와 IsolationLevel은 생략해도 작동했다. 경우에 따라 조금씩 다른듯
}));
describe(....