nestjs、typeORMでカスタムリポジトリのテストを書く

# やりたいこと
nestjs、typeORMの環境でtypeORMのカスタムリポジトリのテストを書きたい。
テストは以下のような感じで実行したい。

- テストデータを用意し、実際に処理を動かしてテストしたい（カスタムリポジトリの関数をmockするテストはしたくない。）
- テストの度にテストデータはリセットされるようにしたい（各テストごとにテストデータを用意する。テストデータの共有はしたくない）

これの参考になるようなコードがあまりネットに見つからなかったので残しておく。

# コード

## テスト対象のentity、repository
このentityのテストを書いていく。

- 外部キーが貼られている
- ユニークキーが貼られている

```ts
import { Entity, Column, PrimaryGeneratedColumn, JoinColumn, ManyToOne, Unique } from 'typeorm';
import { ItemSku } from './itemSku';

@Entity('user_deliverd_sku_relations')
@Unique(['email', 'item_sku_id'])
export class UserDeliverdSkuRelation {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({
    nullable: false,
  })
  email: string;

  @Column({
    nullable: false,
  })
  item_sku_id: number;

  @ManyToOne((type) => ItemSku)
  @JoinColumn({ name: 'item_sku_id' })
  item_sku: ItemSku;

  @Column({
    nullable: false,
    default: () => 'CURRENT_TIMESTAMP',
  })
  created_at: Date;

  @Column({
    nullable: false,
    default: () => 'CURRENT_TIMESTAMP',
  })
  updated_at: Date;
}

```

```ts
import { Repository, EntityRepository } from 'typeorm';
import { UserDeliverdSkuRelation } from '../../entity/userDeliverdSkuRelation';

@EntityRepository(UserDeliverdSkuRelation)
export class DeliveredItemSkuRepository extends Repository<UserDeliverdSkuRelation> {
  findByEmail(email: string) {
    return this.find({
      where: {
        email,
      },
    });
  }
}
```

## リポジトリのテストコード
できたテストコードがこれ

```ts

import { createConnection, getConnection } from 'typeorm';
import { UserDeliverdSkuRelation } from '../../entity/userDeliverdSkuRelation';
import { ItemSku } from '../../entity/itemSku';
import { DeliveredItemSkuService } from '../../service/deliveredItemSku/service';
import { DeliveredItemSkuRepository } from '../../repository/deliveredItemSku';
import { ItemSkuRepository } from '../../repository/itemSku';
import dbConfig from '../../../src/config/database';

describe('DeliveredItemSkuRepository', () => {
  let deliveredItemSkuService: DeliveredItemSkuService;
  let deliveredItemSkuRepository: DeliveredItemSkuRepository;
  let itemSkuRepository: ItemSkuRepository;

  beforeEach(async () => {
    const connection = await createConnection({
      type: dbConfig.type,
      host: dbConfig.host,
      port: parseInt(dbConfig.port, 10),
      username: dbConfig.username,
      password: dbConfig.password,
      database: dbConfig.database,
      entities: [UserDeliverdSkuRelation, ItemSku],
      synchronize: true,
      dropSchema: true,
      logging: false,
      name: dbConfig.connectionName,
    });

    deliveredItemSkuRepository = connection.getCustomRepository(DeliveredItemSkuRepository);
    deliveredItemSkuService = new DeliveredItemSkuService(deliveredItemSkuRepository, connection);

    itemSkuRepository = connection.getCustomRepository(ItemSkuRepository);
  });

  afterEach(async () => {
    await deliveredItemSkuRepository.delete({});
    await itemSkuRepository.delete({});

    await getConnection(dbConfig.connectionName).close();
  });

  describe('定義されている', () => {
    it('リポジトリが定義されている', () => {
      expect(deliveredItemSkuRepository).toBeInstanceOf(DeliveredItemSkuRepository);
    });
  });

  describe('findByEmail', () => {
    it('指定したemailのUserDeliverdSkuRelationエンティティの配列が返ってくる', async () => {
      const testData = [
        {
          id: 1,
          email: 'hoge@example.com',
          item_sku_id: 1,
        },
        {
          id: 1,
          email: 'bar@example.com',
          item_sku_id: 1,
        },
      ] as UserDeliverdSkuRelation[];

      await itemSkuRepository.insert({
        id: 1,
      });

      await deliveredItemSkuRepository.insert(testData);

      const res = await deliveredItemSkuRepository.findByEmail('hoge@example.com');

      expect(res.length).toEqual(1);
      expect(res[0]).toMatchObject({
        id: 1,
        email: 'hoge@example.com',
        item_sku_id: 1,
      });
    });
  });

  describe('insert', () => {
    describe('できるパターン', () => {
      it('insertしたレコードが確認できる', async () => {
        const testData = {
          id: 1,
          email: 'hoge@example.com',
          item_sku_id: 1,
        } as UserDeliverdSkuRelation;

        await itemSkuRepository.insert({
          id: 1,
        });

        await deliveredItemSkuRepository.insert(testData);

        expect(await deliveredItemSkuService.findAll()).toEqual([testData]);
      });
    });

    describe('できないパターン', () => {
      it('外部キー制約でinsertに失敗する。（ひもづくitem_skusがないため）', async () => {
        const testData = {
          id: 1,
          email: 'hoge@example.com',
          item_sku_id: 1,
        } as UserDeliverdSkuRelation;

        await expect(deliveredItemSkuRepository.insert(testData)).rejects.toThrow(
          /insert or update on table "user_deliverd_sku_relations" violates foreign key*/,
        );
      });

      it('ユニークキー制約でinsertに失敗する。（emailとitem_sku_idでユニーク）', async () => {
        await itemSkuRepository.insert({
          id: 1,
        });

        const testData1 = {
          id: 1,
          email: 'hoge@example.com',
          item_sku_id: 1,
        } as UserDeliverdSkuRelation;

        await deliveredItemSkuRepository.insert(testData1);

        const testData2 = {
          id: 2,
          email: 'hoge@example.com',
          item_sku_id: 1,
        } as UserDeliverdSkuRelation;

        await expect(deliveredItemSkuRepository.insert(testData2)).rejects.toThrow(
          /duplicate key value violates unique constraint*/,
        );
      });
    });
  });
});

```

# ポイント

## 各テスト実行前
beforeEachで以下を行なっている。

```ts
  beforeEach(async () => {
    const connection = await createConnection({
      type: dbConfig.type,
      host: dbConfig.host,
      port: parseInt(dbConfig.port, 10),
      username: dbConfig.username,
      password: dbConfig.password,
      database: dbConfig.database,
      entities: [UserDeliverdSkuRelation, ItemSku],
      synchronize: true,
      dropSchema: true,
      logging: false,
      name: dbConfig.connectionName,
    });
```

これによって、以下がテストの度に実行されるようになる。

- DBとの接続
createConnectionで接続し直す

- テーブルのdrop
`dropSchema: true` によって接続が確立される度にテーブルをdropする。対象はentitiesに記載されたentity。

- マイグレーション
`synchronize: true` によってマイグレーションを流し直す。

これでテストのたびにまっさらなテーブル(UserDeliverdSkuRelation, ItemSku)が用意されるようになる。

## 各テスト実行後
afterEachでテストに関わるentityのデータを削除をする。
beforeEachの処理によって、テスト実行前のテストデータ削除はできているが、テスト実行後にもテストデータを削除しておくことで他のテストへ影響を与えないようにする。

```ts
  afterEach(async () => {
    await deliveredItemSkuRepository.delete({});
    await itemSkuRepository.delete({});

    await getConnection(dbConfig.connectionName).close();
  });
```



これで当初やりたかった `テストの度にテストデータはリセットされる` ができてると思う。



# 参考
https://stackoverflow.com/questions/55366037/inject-typeorm-repository-into-nestjs-service-for-mock-data-testing