【NestJS】factoryで実データを作ってDB層のテストをする

# 環境

- [NestJS 7.0.7](https://nestjs.com/)
- [nestjs/typeorm 7.0.0](https://github.com/nestjs/typeorm)
- [typeorm 0.2.25](https://github.com/typeorm/typeorm)
- [factory.ts 0.5.1](https://github.com/willryan/factory.ts)

# やりたいこと/やること

- DB層にアクセスする処理（SQL）を、実際のデータを使ってテストする。
- `factory.ts`を使ってオブジェクトを生成し、事前にDBにsaveすることにより、テストデータを用意する。
- 各テストが独立して動作するように、テスト毎にテーブルを空にする。

# factory.tsを選んだ理由

- [typeorm-fixtures](https://github.com/RobinCK/typeorm-fixtures)も存在し、2020/4時点でこちらの方がStar数も多く（factory.tsの120に対して221）、最終更新日も最近だったが（2019/12に対して2020/4）、以下の理由で`factory.ts`を使うことにした。
  - factory と fixture の比較で、factory が好みだった（テスト毎に独立したデータを用意したかった）。
  - `typeorm-fixtures`は、CLIで使う前提。[プログラム内でデータをロードすることも可能](https://github.com/RobinCK/typeorm-fixtures#programmatically-loading-fixtures)だが、「ディレクトリ配下のデータをロードする」という動作となるため、データの一括登録となるのが好みではなかった。

# factory.tsで少し手間な点

- 「save したデータを返す」という機能がないので、毎回自身で save する必要がある。
- [factory-girl](https://github.com/simonexmachina/factory-girl) では、save 済のデータを返す機能もあるようなので、試してみたい。
  - Star は多いが（2020/8 時点で、484）、最終更新日時が `22 Nov 2018` となっているのは気になる。

# 前提（Entityの定義）

`User has_many Works` として、以下のような Entity が存在するとします。
User は lyricist（作詞家）または composer（作曲家）として、Work と関連が存在します。

```typescript:src/users/user.entity.ts
// import は割愛

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  readonly id!: number

  @Column()
  name!: string

  @OneToMany(
    () => Work,
    (work) => work.lyricist,
    { cascade: true }
  )
  worksAsLyricist!: Work[]

  @OneToMany(
    () => Work,
    (work) => work.composer,
    { cascade: true }
  )
  worksAsComposer!: Work[]

  @CreateDateColumn()
  readonly createdAt!: Date

  @UpdateDateColumn()
  readonly updatedAt!: Date
}
```

```typescript:src/works/work.entity.ts
// import は割愛

@Entity()
export class Work {
  @PrimaryGeneratedColumn()
  readonly id!: number

  @Column()
  title!: string

  @Column()
  isInstrumental!: boolean

  @Index()
  @ManyToOne(
    () => User,
    (lyricist) => lyricist.workAsLyricist
  )
  lyricist!: User

  @Index()
  @ManyToOne(
    () => User,
    (composer) => composer.workAsLyricist
  )
  owner!: User

  @CreateDateColumn()
  readonly createdAt!: Date

  @UpdateDateColumn()
  readonly updatedAt!: Date
}
```

# factory.tsの定義

```typescript:factories/user.factory.ts
import * as Factory from 'factory.ts'

export const userFactory = Factory.makeFactory<User>({
  Object.assign(new User(), {
    name: 'myNameIsWho'
    createdAt: Factory.each(() => new Date()),
    updatedAt: Factory.each(() => new Date()),
  })
})
```

```typescript:factories/work.factory.ts
import * as Factory from 'factory.ts'

const user = userFactory.build()

export const workFactory = Factory.makeFactory<Work>({
  Object.assign(new Work(), {
    title: 'name',
    isInstrumental: false,
    lyricist: user,
    composer: user,
    createdAt: Factory.each(() => new Date()),
    updatedAt: Factory.each(() => new Date()),
  })
})

// extend を使うことで、既存の factory の拡張 （値の変更、カラムの追加） ができます。
export const instrumentalWorkFactory = workFactory.extend({
  isInstrumental: true,
})
```

```typescript:factories/work.factory.ts
// work.factory.ts の別の定義例。
// combine を使って複数の factory を適用する。

const timeStamps = Sync.makeFactory({
  createdAt: Sync.each(() => new Date()),
  updatedAt: Sync.each(() => new Date())
})

const instrumental = Sync.makeFactory({
  isInstrumental: true
})

export const workFactory = Factory.Sync.makeFactory<Work>({
  Object.assign(new Work(), {
    lyricist: user,
    composer: user,
  })
})
  .combine(timeStamps)
  .combine(instrumental)
```

# データの生成

```typescript
// factory に定義されたオブジェクトを生成する （DBには保存されていません）
const userObj = userFactory.build()

// 一部値を変えてオブジェクトを生成する
const userObj1 = userFactory.build({ name: 'foo' })

// 複数のオブジェクトを生成する
const usersObj = userFactory.buildList(2)

// 生成したオブジェクトをDBに保存する。
const user = await userRepo.save(userObj)
// or
const user = await userRepo.save(userFactory.build())
```

# テスト内での使い方

`work.repository.ts`、`user.repository.ts`として、カスタムリポジトリを定義しており、そこでDB層へのアクセスを行っていると仮定します。

```typescript:src/works/test/work.repository.spec.ts
// import は割愛

describe('workRepository', () => {
  let module: TestingModule
  let workRepo: WorkRepository
  let userRepo: UserRepository

  beforeAll(async () => {
    module = await Test.createTestingModule({
      imports: [
        TypeOrmModule.forRoot(config.get('database')),
        TypeOrmModule.forFeature([WorkRepository, UserRepository]),
      ],
    }).compile()

    workRepo = module.get(WorkRepository)
    userRepo = module.get(UserRepository)
  })

  afterEach(async () => {
    // テスト毎に、テーブル内のデータを削除する。
    await getConnection().synchronize(true)
  })

  afterAll(async () => {
    // テストの最後に connection を close する。
    module.close()
  })

  describe('getWorks', () => {
    let user1: User
    let user2: User
    let work1: Work
    let work2: Work

    beforeEach(async () => {
      user1 = await userRepo.save(
        userFactory.build({ name: 'foo' })
      )
      user2 = await userRepo.save(
        userFactory.build({ name: 'bar' })
      )

      work1 = await workRepo.save(
        workFactory.build({ lyricist: user1, composer: user2 })
      )
      work2 = await workRepo.save(
        workFactory.build({ lyricist: user1, composer: user1 })
      )
    })

    it('returns Work[] belonging to the composer with the name `foo`', async () => {
      const filterDto = new GetWorksFilterDto()
      filterDto.name = user1.name

      const result = await workRepo.getWorks(filterDto)

      expect(result.length).toBe(1)
      expect(result[0].id).toBe(user1.id)
      expect(result[0].name).toBe(user1.name)
    })
  })
})
```

# 備忘録（追記すること）

- [Async Factories](https://github.com/willryan/factory.ts#async-factories) について