Framework-agnostic model factory system for clean testing.
Built-on top of Knex + Faker, and heavily inspired by Adonis.js and Laravel.
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 Factorio, you can extract all this set up to a dedicated file and then write the bare minimum code to set up the database state.
馃毀 This is a work in progress. v1 is not ready, some features are missing. Expect major API changes without following semver. 馃毀
- Support for multiple databases ( SQLite, Postgres, MySQL, MSSQL ... )
- Integrations with test runners
- Define variations of your model using states
- Define relations
- Installation
- Integrations
- Defining database connection
- Creating factories
- Using factories
- Merging attributes
- Factory states
- Relationships
pnpm install @julr/factorio
Integrations for some test runners are available below :
Before running your tests, you must initialize Factorio with your database configuration.
This must be done BEFORE creating models via Factorio. In general, you can use the setup files system provided by the test runners.
const disconnect = defineFactorioConfig({
// Can also specify a locale for faker
locale: 'fr',
// See https://knexjs.org/guide/#configuration-options
// for more information
database: {
client: 'pg',
connection: {
host: 'localhost',
user: 'root',
password: 'password',
database: 'factorio',
}
}
})
// Once you are done with your tests, you can disconnect from the database
await disconnect()
defineFactorioConfig
returns a function that can be used to disconnect from the database.
This is useful when you want to cleanly disconnect from the database after all tests have been run.
Note: You don't need to do this manually if you are using a test runner integration.
import type { User } from './my-user-interface.js'
const UserFactory = defineFactory<Partial<User>>(({ faker }) => ({
tableName: 'user',
fields: {
email: faker.internet.email(),
referralCode: faker.random.alphaNumeric(6)
},
})).build()
Make sure that you return an object with all the required properties, otherwise the database will raise not null exceptions.
import { UserFactory } from './my-factory.js'
const user = await UserFactory.create()
const users = await UserFactory.createMany(10)
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 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.
export const PostFactory = defineFactory<Partial<Post>>(({ faker }) => ({
tableName: 'post',
fields: {
title: faker.lorem.sentence(),
content: faker.lorem.paragraphs(4),
status: 'DRAFT',
}
}))
.state('published', (attributes) => ({
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)
Model factories makes it super simple to work with relationships. Consider the following example:
export const PostFactory = defineFactory<Partial<Post>>(({ faker }) => ({
tableName: 'post',
fields: {
title: faker.lorem.sentence(),
content: faker.lorem.paragraphs(4),
status: 'DRAFT',
}
}))
.state('published', (attributes) => ({
status: 'PUBLISHED', // 馃憟
}))
.build()
export const UserFactory = defineFactory<Partial<User>>(({ faker }) => ({
tableName: 'user',
fields: {
username: faker.internet.userName(),
email: faker.internet.email(),
password: faker.internet.password(),
}
}))
.hasMany('posts', { foreignKey: 'user_id', localKey: 'id', factory: () => 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
Note that the foreignKey
and localKey
are optionals. If they are not defined, Factorio will try to guess them based upon the model name.
By default, the foreignKey
is {tableName}_id
and the localKey
is id
.
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()
The followings are the available relationships:
hasOne
hasMany
belongsTo
manyToMany
( 馃毀 coming soon )