6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-04-26

環境

やりたいこと/やること

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

factory.tsを選んだ理由

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

factory.tsで少し手間な点

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

前提(Entityの定義)

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

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
}
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の定義

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()),
  })
})
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,
})
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)

データの生成

// 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.tsuser.repository.tsとして、カスタムリポジトリを定義しており、そこでDB層へのアクセスを行っていると仮定します。

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)
    })
  })
})

備忘録(追記すること)

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?