TypeORM Transaction을 Test하기 (with queryRunner)

이우길·2022년 6월 16일
4

NestJs

목록 보기
12/20
post-thumbnail

TypeORM Transaction을 Test하기

예제코드는 Github에 있습니다:)

Goal

  • TypeORM의 queryRunner를 이용하여 Transaction을 이용하였을 때 jest를 이용하여 Test 해보기

Version

현 시점에서 최신의 @nestjs/typeorm에서는 TypeORM의 버전을 0.3.x를 이용하고 있으나 해당 글에서는 ^0.2.45를 기준으로 작성할 것이다. (차이점은 Connection을 이용하는 것과 DataSource를 이용하는 것이다.)


Why

이전 글에서 pg-mem을 이용하여 TypeOrm 트랜잭션을 테스트 할 수 있을 줄 알았지만 pg-mem에서 ROLLBACK을 지원하지 않아 실패하였다.

그렇기 때문에 jest를 이용하여 QueryRunnerMocking하여 Test를 진행하기로 하였다. Test를 하기 위해서 필요한 객체는 아래와 같다.

  1. QueryRunner -> interface

  2. EntityManager -> class

  3. Connection -> class


QueryRunner 대역 만들기

이전 글에서 작성한 것과 동일하게 createQueryRunner()를 이용하여 트랜잭션을 사용하게 된다.

QueryRunner는 interface type이며 내부적으로 manager라는 프로퍼티를 가지고 있다.

// manager만 가지고 있는 것은 아니다.
export interface QueryRunner {
  /**
   * Entity manager working only with this query runner.
   */
  readonly manager: EntityManager;
}

QueryRunner는 아래의 코드처럼 간단하게 대역을 만들 수 있다.

const qr = {
  manager: {},
} as QueryRunner;

이 후 QueryRunner를 이용하여 트랜잭션을 사용하기 때문에 필요한 methodjest.fn()을 이용하여 Mocking한다.

qr.startTransaction = jest.fn();
qr.commitTransaction = jest.fn();
qr.rollbackTransaction = jest.fn();
qr.release = jest.fn();

EntityManager method Mocking하기

QueryRunner에 있는 manager(EntityManager)는 기본적으로 사용하는 CRUD method가 존재한다.

Test하고자 하는 코드에서 사용하는 method를 Object.assign을 통하여 manager(EntityManager)에 넣어준다. (Object.assign)

Object.assign(qr.manager, { save: jest.fn() });

Connection 대역 만들기

Connection에서 QueryRunner를 생성하는 method의 syntax는 아래의 코드와 같다.

createQueryRunner(mode?: ReplicationMode): QueryRunner;

해당 method를 가지고 있는 MockConnection class를 만들어 준다. return 값은 QueryRunner인데 위에서 만든 QueryRunner의 대역을 return해주면 된다.

class ConnectionMock {
  createQueryRunner(mode?: "master" | "slave"): QueryRunner {
    return qr;
  }
}

최종적인 모습

위의 3개의 대상들을 Mocking하면 테스트 준비가 끝나게된다. 3개의 단계를 합치게 되면 아래와 같은 코드가 완성된다.

describe('Transaction Unit Test', () => {
  let usersService: UsersService;
  let connection: Connection;

  const qr = { // 1
    manager: {},
  } as QueryRunner;

  class ConnectionMock { // 2
    createQueryRunner(mode?: 'master' | 'slave'): QueryRunner {
      return qr;
    }
  }

  beforeEach(async () => {
    Object.assign(qr.manager, { save: jest.fn() }); // 3

    // 4
    qr.startTransaction = jest.fn();
    qr.commitTransaction = jest.fn();
    qr.rollbackTransaction = jest.fn();
    qr.release = jest.fn();

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: Connection,
          useClass: ConnectionMock, // 5
        },
        {
          provide: getRepositoryToken(Users),
          useClass: Repository,
        },
      ],
    }).compile();

    usersService = module.get<UsersService>(UsersService);
    connection = module.get<Connection>(Connection);
  });

  //...
}
  1. QueryRunner의 대역을 만든다.

  2. Connection의 대역을 만든다.

  3. Test를 할 때 필요한 TypeORM method를 Mocking한다.

  4. 트랜잭션에 필요한 method들을 Mocking한다.

  5. Connection의 대역을 CustomProvider를 이용하여 Provider로 정의한다.


Test 하기

User를 트랜잭션과 함께 저장하는 로직은 아래와 같이 간단하게 작성하였다.

async saveWithQueryRunner(user: Users): Promise<Users> {
    const queryRunner = this.connection.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      const saved = await queryRunner.manager.save(user);
      //...
      await queryRunner.commitTransaction();
      return saved;
    } catch (e) {
      await queryRunner.rollbackTransaction();
    } finally {
      await queryRunner.release();
    }
  }

위의 코드를 검증하는 실패case와 성공case TestCode는 아래와 같다. (트랜잭션 테스트를 하는 것을 주제로 하고 있기에 jest에 대해서는 설명하지 않겠다.)


테스트 코드에서 중요하게 볼 것은 const queryRunner = connection.createQueryRunner();로 이전 테스트를 하기 위한 사전작업에서 정의한 QueryRunner를 가져오는 것, manager를 이용하여 CRUD method를 Mocking하는 것이다.

it("정상적으로 저장되는 경우", async () => {
  //given
  const now: Date = new Date();
  const willSavedUser: Users = {
    id: 1,
    name: "foobar",
    createdAt: now,
    lastModifiedAt: now,
  };
  const queryRunner = connection.createQueryRunner();

  jest.spyOn(queryRunner.manager, "save").mockResolvedValueOnce(willSavedUser);

  //when
  const result = await usersService.saveWithQueryRunner(new Users());

  //then
  expect(result).toStrictEqual(willSavedUser);
  expect(queryRunner.manager.save).toHaveBeenCalledTimes(1);
  expect(queryRunner.commitTransaction).toHaveBeenCalledTimes(1);
  expect(queryRunner.release).toHaveBeenCalledTimes(1);
});

it("save 도중 Error가 발생하는 경우", async () => {
  //given
  const now: Date = new Date();
  const willSavedUser: Users = {
    id: 1,
    name: "foobar",
    createdAt: now,
    lastModifiedAt: now,
  };
  const queryRunner = connection.createQueryRunner();

  jest
    .spyOn(queryRunner.manager, "save")
    .mockRejectedValueOnce(new Error("DataBase Error 발생"));

  const result = await usersService.saveWithQueryRunner(new Users());

  expect(result).toBeUndefined();
  expect(queryRunner.rollbackTransaction).toHaveBeenCalledTimes(1);
  expect(queryRunner.release).toHaveBeenCalledTimes(1);
});

테스트를 실행하면 잘 동작하는 것을 확인할 수 있다.


REFERENCE

profile
leewoooo

1개의 댓글

comment-user-thumbnail
2022년 6월 17일

잘봤습니다 ~ :)

답글 달기