模型工厂
你是否编写过测试,其中每个测试的前 15-20 行 专门用于通过使用多个模型来设置数据库状态?使用模型工厂,你可以将所有这些设置提取到一个专用文件中,然后编写最少的代码来设置数据库状态。
在本指南结束时,你将了解:
- 如何创建和使用工厂
- 如何定义工厂状态
- 处理模型关系
- 使用 faker API 生成和使用随机数据
创建工厂
模型工厂存储在 databases/factories
目录中。你可以在单个文件中定义所有工厂或为每个模型创建专用文件,选择权在你手中。
注:你可以使用
make:factory
命令创建一个新工厂。该命令接受你要为其创建工厂的模型名称。
与播种机或模型不同,工厂本质上是声明性的,如下例所示:
// title: database/factories/index.ts
import User from 'App/Models/User'
import Factory from '@ioc:Adonis/Lucid/Factory'
export const UserFactory = Factory
.define(User, ({ faker }) => {
return {
username: faker.internet.userName(),
email: faker.internet.email(),
password: faker.internet.password(),
}
})
.build()
Factory.define
方法总共接受两个参数。- 第一个参数是对 Lucid 模型的引用。
- 第二个参数是一个回调,它返回在持久化模型实例时要使用的属性对象。确保返回具有所有必需属性的对象,否则数据库将引发
not null
异常。 - 最后,确保调用
build
方法。
使用工厂
使用工厂非常简单, 只需 import
文件并使用其导出的工厂。
import { UserFactory } from 'Database/factories'
const user = await UserFactory.create()
为了创建多个实例,你可以使用 createMany
方法。
const users = await UserFactory.createMany(10)
合并属性
你可以使用 .merge
方法合并默认属性集合,如:
await UserFactory
.merge({ email: 'test@example.com' })
.create()
当创建多个实例时,你可以定义一个属性数组,它将根据索引进行合并,如:
await UserFactory
.merge([
{ email: 'foo@example.com' },
{ email: 'bar@example.com' },
])
.createMany(3)
在上面的例子中:
- 第一个用户将是
foo@example.com
的邮件地址 - 第二个用户将是
bar@example.com
的邮件地址 - 由于合并的数组长度为2,第三个用户将使用默认的邮件地址
工厂状态
工厂状态允许你将工厂的变体定义为状态。如:在 Post
工厂中,你可以用不同的状态来表示已经发布的贴子和草稿。
import Factory from '@ioc:Adonis/Lucid/Factory'
import Post from 'App/Models/Post'
export const PostFactory = Factory
.define(Post, ({ faker }) => {
return {
title: faker.lorem.sentence(),
content: faker.lorem.paragraphs(4),
status: 'DRAFT',
}
})
.state('published', (post) => post.status = 'PUBLISHED') // 👈
.build()
默认情况下,所有帖子都会以 DRAFT
状态创建,但你也可以显式指定 published
状态来创建具有 PUBLISHED
状态的帖子。
await PostFactory.apply('published').createMany(3)
await PostFactory.createMany(3)
关系
模型工厂使处理关系变得超级简单。考虑以下示例:
export const PostFactory = Factory
.define(Post, ({ faker }) => {
return {
title: faker.lorem.sentence(),
content: faker.lorem.paragraphs(4),
status: 'DRAFT',
}
})
.build()
export const UserFactory = Factory
.define(User, ({ faker }) => {
return {
username: faker.internet.userName(),
email: faker.internet.email(),
password: faker.internet.password(),
}
})
.relation('posts', () => PostFactory) // 👈
.build()
现在,你可以在一次调用中同时创建一个 user
及其 posts
。
const user = await UserFactory.with('posts', 3).create()
user.posts.length // 3
注意事项
- 工厂将通过检查 Lucid 模型找到关系类型。例如:如果你的模型在
posts
上定义了hasMany
关系,那么工厂将推断出相同的关系。 - 首先需要在模型上定义关系,然后才能在工厂上定义它。
- Lucid 将在内部将所有数据库操作包装在事务中。因此,如果关系不持续存在,父模型也将回滚。
应用关系状态
你还可以通过将回调传递给 with
方法来对关系应用状态。
const user = await UserFactory
.with('posts', 3, (post) => post.apply('published'))
.create()
同样,如果你愿意,可以创建一些带有 published
状态的帖子,而没有它的帖子很少。
const user = await UserFactory
.with('posts', 3, (post) => post.apply('published'))
.with('posts', 2)
.create()
user.posts.length // 5
最后,你还可以创建嵌套关系。例如:创建一个具有两个帖子和每个帖子五个评论的用户。
const user = await UserFactory
.with('posts', 2, (post) => post.with('comments', 5))
.create()
透视属性
创建 多对多 关系时,你可以使用 pivotAttributes
方法定义数据透视表的属性。
在以下示例中,User
模型与 Team
模型具有多对多关系,我们在指定团队中定义用户角色。
await UserFactory
.with('teams', 1, (team) => {
team.pivotAttributes({ role: 'admin' })
})
.create()
在创建关系的多个实例时,你可以将对象数组传递给 pivotAttributes
方法。
注:数组的大小应与你即将创建的关系行数相匹配。
await UserFactory
.with('teams', 2, (team) => {
team.pivotAttributes([
{ role: 'admin' },
{ role: 'moderator' }
])
})
.create()
存根数据库调用
在某些情况下,你可能更喜欢将数据库调用存根,只想创建内存中的模型实例。这可以使用 makeStubbed
和 makeStubbedMany
方法来实现。
const user = await UserFactory
.with('posts', 2)
.makeStubbed()
console.log(user.id) // <some-id>
console.log(user.$isPersisted) // false
存根调用永远不会命中数据库,并将分配一个内存中的数字 id
给模型实例。
自定义存根 id
备注:
当我们说id
时,指的是模型的主键,而不是固定的命名属性id
。
存根 id 只是一个内存中的计数器,它会随着每次调用而不断增加。如果需要,你可以自定义方法以不同方式生成存根 ID。
例如:在使用 PostgreSQL bigInteger
数据类型时将 ids 生成为 BigInt
。
import Factory from '@ioc:Adonis/Lucid/Factory'
Factory.stubId((counter, model) => {
return BigInt(counter)
})
你可以使用 makeStubbed
挂钩来自定义单个工厂的 id 生成行为。
Factory
.define(Post, () => {
return {}
})
.before('makeStubbed', (_, model) => {
model.id = uuid.v4()
})
运行时上下文
每次从工厂创建模型实例时,也会同时创建运行时上下文。然后将上下文传递给所有钩子、define
方法回调以及关系。
大多数时候,你只想从上下文中访问 faker
对象。以下是可用的属性:
- isStubbed:一个布尔值,知道工厂是否在存根模式下实例化。
- $trx:一个事务对象,所有的数据库操作都封装在它之下。如果你在工厂挂钩内运行任何数据库查询,请确保也将它们包装在事务中。
以下示例展示了接收运行时上下文 (ctx)
的回调。
Factory
.define(User, (ctx) => {
})
.before('create', (factory, model, ctx) => {
})
.after('create', (factory, model, ctx) => {
})
.state('admin', (model, ctx) => {
})
.build()
钩子
工厂公开了以下钩子来对某些事件发生之前或之后执行操作,你还可以为单个事件定义多个挂钩。
Factory
.define(Post, () => {})
.before('create', () => {})
.after('create', () => {})
生命周期 | 事件 | 描述 |
---|---|---|
before |
create |
在插入查询之前调用 |
after |
create |
在插入查询之后调用 |
before |
makeStubbed |
在存根调用之前调用 |
after |
makeStubbed |
在存根调用之后调用 |
after |
make |
仅在创建模型实例之后调用,这个钩子也在 before create 和 before makeStubbed 钩子之前被调用 |
自定义连接
工厂允许你在使用它们时定义自定义连接或查询客户端。例如:
await Factory.connection('tenant-1').create()
你也可以传递自定义查询客户端实例。
const queryClient = Database.connection('tenant-1')
await Factory.client(queryClient).create()
为了工厂和 Lucid 模型之间的 API 统一,你还可以使用 query
方法定义 connection
或 client
。
await Factory.query({ connection: 'tenant-1' }).create()
自定义
最后,你可以选择自定义在后台执行的某些操作的行为。
newUp
通过定义 newUp
处理程序,可以自定义为给定工厂实例化模型实例的过程。
Factory
.define(User, () => {
})
.newUp((attributes, ctx) => {
const user = new User()
user.fill(attributes)
return user
})
.build()
merge
通过定义 merge
处理程序,可以自定义合并行为。
Factory
.define(User, () => {
})
.merge((user, attributes, ctx) => {
user.merge(attributes)
})
.build()
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。