타입스크립트 꽝꽝

[TypeScript] ts-mockito 로 테스트하기. 그런데 트랜잭션을 곁들인

bimppap 2022. 3. 4. 17:30

개발환경

- 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 에러가 뜬다.

하라는 대로 했더니 산 넘어 산;;

해당 에러에 대해 서치하면서 스택오버플로우 QnATypeORM 이슈를 모두 살펴봤지만 둘 다 큰 도움은 되지 않았다.

디버깅하면서 확인해보니 typeorm-transactional-cls-hooked의 Transactional 데코레이터를 인지하면 테스트 모듈 설정을 받는데... 도중에 undefined가 입력되면서 뜨는 에러였다. 그럼 이 부분을 모킹하면 해결될까? 해서 찾아보았다. 그리고...

 

https://github.com/odavid/typeorm-transactional-cls-hooked/issues/2

 

Mock Transactional decorator · Issue #2 · odavid/typeorm-transactional-cls-hooked

Hello there, Is there any way to mock Transactional decorator? If so, how?

github.com

 

위 링크에서 해답을 찾을 수 있었다!!!! 😊

아래처럼 describe 전에 Trasactional 을 모킹하면 해결되는 문제였다. (간략하게 썼지만 이걸 해결하기 위해 이틀을 날렸다...🥲)

// imports
...
jest.mock('typeorm-transactional-cls-hooked', () => ({
  Transactional: () => () => ({}),
  BaseRepository: class {},
  IsolationLevel: { SERIALIZABLE: 'SERIALIZABLE' },
  // 내 경우 BaseRepository와 IsolationLevel은 생략해도 작동했다. 경우에 따라 조금씩 다른듯
}));

describe(....