Skip to content

Latest commit

History

History
231 lines (174 loc) 路 6.61 KB

README.md

File metadata and controls

231 lines (174 loc) 路 6.61 KB

@julr/factorio

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. 馃毀

Features

  • Support for multiple databases ( SQLite, Postgres, MySQL, MSSQL ... )
  • Integrations with test runners
  • Define variations of your model using states
  • Define relations

Table of Contents

Installation

pnpm install @julr/factorio

Integrations

Integrations for some test runners are available below :

Defining database connection

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.

Creating factories

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.

Using factories

import { UserFactory } from './my-factory.js'

const user = await UserFactory.create()
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.

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)

Relationships

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.

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

The followings are the available relationships:

  • hasOne
  • hasMany
  • belongsTo
  • manyToMany ( 馃毀 coming soon )