環境
やりたいこと/やること
- 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
となっているのは気になる。
- Star は多いが(2020/8 時点で、484)、最終更新日時が
前提(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.ts
、user.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)
})
})
})
備忘録(追記すること)
- Async Factories について