Model factories
Have you ever written tests, in which the first 15-20 lines of each test are dedicated to just setting up the database state by using multiple models? With model factories, you can extract all this set up to a dedicated file and then write the bare minimum code to set up the database state.
By the end of this guide, you will know:
- How to create and use factories
- How to define factory states
- Working with model relationships
- Using the faker API to generate and use random data
Creating factories
Model factories are stored inside databases/factories
directory. You can define all factories within a single file or create dedicated files for each model, the choice is yours.
You can use the make:factory
command to create a new factory. The command accepts the model name for which you want to create the factory.
Unlike seeders or models, the factories are declarative in nature as shown in the following example:
import User from '#models/user'
import Factory from '@adonisjs/lucid/factories'
export const UserFactory = Factory.define(User, ({ faker }) => {
return {
username: faker.internet.userName(),
email: faker.internet.email(),
password: faker.internet.password(),
}
}).build()
- The
Factory.define
method accepts a total of two arguments. - The first argument is reference to the Lucid model.
- The second argument is a callback that returns an object of properties to be used when persisting the model instance. Make sure that you return an object with all the required properties, otherwise the database will raise
not null
exceptions. Thefaker
option passed into the callback is an instance of Faker.js - Finally, make sure to call the
build
method.
Using factories
Using factories is quite simple. Just import
the file and use the exported factories.
import { UserFactory } from '#database/factories/user'
const user = await UserFactory.create()
In order to create multiple instances, you can make use of the createMany
method.
const users = await UserFactory.createMany(10)
Merging attributes
You can override the default set of attributes using the .merge
method. For example:
await UserFactory.merge({ email: 'test@example.com' }).create()
When creating multiple instances, you can define an array of attributes and they will merge based upon their indices. For example:
await UserFactory.merge([{ email: 'foo@example.com' }, { email: 'bar@example.com' }]).createMany(3)
In the above example
- The first user will have the email of
foo@example.com
. - The second user will have the email of
bar@example.com
. - And, the third user will use the default email address, since the merge array has a length of 2.
Factory states
Factory states allow you to define variations of your factories as states. For example: On a Post
factory, you can have different states to represent published and draft posts.
import Post from '#models/post'
import { Factory } from '@adonisjs/lucid/factories'
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()
By default, all posts will be created with DRAFT
status. However, you can explicitly apply the published
state to create posts with PUBLISHED
status.
await PostFactory.apply('published').createMany(3)
await PostFactory.createMany(3)
Relationships
Model factories makes it super simple to work with relationships. Consider the following example:
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()
Now, you can create a user
and its posts
all together in one call.
const user = await UserFactory.with('posts', 3).create()
user.posts.length // 3
-
The factory will find the type of relationship by inspecting the Lucid model. For example: If your model defines a
hasMany
relationship onposts
, then factory will infer the same. -
A relationship first needs to be defined on the model and then only it can be defined on the Factory.
-
Lucid will internally wrap all the database operations inside a transaction. So if a relationship persistence fails, the parent model persistence will be rolled back too.
Applying relationship states
You can also apply states on a relationship by passing a callback to the with
method.
const user = await UserFactory.with('posts', 3, (post) => post.apply('published')).create()
Similarly, if you want, you can create few posts with the published
state and few without it.
const user = await UserFactory.with('posts', 3, (post) => post.apply('published'))
.with('posts', 2)
.create()
user.posts.length // 5
Finally, you can also create nested relationships. For example: Create a user with two posts and five comments for each post.
const user = await UserFactory.with('posts', 2, (post) => post.with('comments', 5)).create()
Pivot attributes
When creating a many to many relationship, you can define the attributes for the pivot table using the pivotAttributes
method.
In the following example, the User
model has a many to many relationship with the Team
model and we define the user role within a given team.
await UserFactory.with('teams', 1, (team) => {
team.pivotAttributes({ role: 'admin' })
}).create()
You can pass an array of objects to the pivotAttributes
method when creating multiple instances of the relationship.
The size of array should match the count of relationship rows you are about to create.
await UserFactory.with('teams', 2, (team) => {
team.pivotAttributes([{ role: 'admin' }, { role: 'moderator' }])
}).create()
Stubbing database calls
In some cases, you may prefer to stub out the database calls and just want to create in-memory model instances. This is can achieved using the makeStubbed
and makeStubbedMany
methods.
const user = await UserFactory.with('posts', 2).makeStubbed()
console.log(user.id) // <some-id>
console.log(user.$isPersisted) // false
The stubbed calls will never hit the database and will assign an in-memory numeric id
to the model instances.
Customizing stub id
When we say id
. We mean the primary key of a model and not a fixed named attribute id
.
The stub id is just an in-memory counter, that keeps on increasing with every call. If required, you can define a custom method to generate stub ids in a different manner.
For example: Generating ids as a BigInt
when using PostgreSQL bigInteger
data type.
import { Factory } from '@adonisjs/lucid/factories'
Factory.stubId((counter, model) => {
return BigInt(counter)
})
You can make use of the makeStubbed
hook to customize the id generation behavior for an individual factory.
import { randomUUID } from 'node:crypto'
Factory.define(Post, () => {
return {}
}).before('makeStubbed', (_, model) => {
model.id = randomUUID()
})
Runtime context
Every time you create a model instance from a factory, a runtime context is also created at the same time. The context is then passed to all the hooks, the define
method callback and also the relationships.
Most of the time, you just want to access the faker
object from the context. However, following are the available properties.
- isStubbed: A boolean to know, if the factory was instantiated in stub mode.
- $trx: A transaction object, under which all the database operations are wrapped. If you are running any database queries inside the factory hooks, then make sure to also wrap them inside the transaction.
Following is an example showcasing the callbacks that receives the runtime context (ctx)
.
Factory.define(User, (ctx) => {})
.before('create', (factory, model, ctx) => {})
.after('create', (factory, model, ctx) => {})
.state('admin', (model, ctx) => {})
.build()
Hooks
The factory exposes the following hooks to perform actions before
or after
certain events. You can also define multiple hooks for a single event.
Factory.define(Post, () => {})
.before('create', () => {})
.after('create', () => {})
Lifecycle | Event | Description |
---|---|---|
before | create | Invoked before the insert query. |
after | create | Invoked after the insert query. |
before | makeStubbed | Invoked before the stubbed call. |
after | makeStubbed | Invoked after the stubbed call. |
after | make | Invoked only after the model instance has been created. This hook is also invoked before the before create and before makeStubbed hooks. |
Custom connections
Factories allows you to define a custom connection or query client at the time using them. For example:
await Factory.connection('tenant-1').create()
Also, you can pass a custom query client instance.
const queryClient = Database.connection('tenant-1')
await Factory.client(queryClient).create()
For the sake of API uniformity among the factories and the Lucid models, you can also define the connection
or the client
using the query
method.
await Factory.query({ connection: 'tenant-1' }).create()
Customizations
Finally, you can optionally customize the behavior of certain operations performed under the hood.
newUp
By defining the newUp
handler, you can customize the process of instantiating a model instance for a given factory.
Factory.define(User, () => {})
.newUp((attributes, ctx) => {
const user = new User()
user.fill(attributes)
return user
})
.build()
merge
By defining the merge
handler, you can customize the merge behavior.
Factory.define(User, () => {})
.merge((user, attributes, ctx) => {
user.merge(attributes)
})
.build()