Factorify Documentation

website·Indexed 32 days ago

https://factorify.julr.dev/

A tool for generating in-memory instances and defining factories for testing backend applications, featuring support for states, relationships, and a Japa integration via the @julr/japa-database-plugin.

Tokens
2.6K
Snippets
16
Records
18
Agent score
54%

What's inside Factorify

  1. Overview of Factorify for database test setup

    Factorify is a framework-agnostic library designed to reduce boilerplate code in tests by extracting database state setup into dedicated factory files. It is built on top of Knex and Faker, drawing inspiration from Adonis.js and Laravel. It supports multiple database engines including SQLite, Postgres, MySQL, and MSSQL, and can be used with any test runner or framework.
  2. Stub models in-memory without database calls

    Use the make and makeMany methods to create in-memory model instances without hitting the database. These methods assign a numeric ID to the instances in-memory, which is useful for unit tests where database persistence is not required.
    // Create an in-memory user with 2 related posts (no DB calls)
    const user = await UserFactory
      .with('posts', 2)
      .make()
    
    console.log(user.id) // In-memory numeric ID
  3. Create model instances using factories

    Use the create and createMany methods to persist model instances to the database. Both methods are asynchronous and return the created records, making them suitable for use in tests.
    import { UserFactory } from './my-factory.js'
    
    // Create a single record
    const user = await UserFactory.create()
    
    // Create multiple records
    const users = await UserFactory.createMany(10)
  4. Install and configure Factorify for Japa

    To use Factorify with the Japa test runner, install the @julr/japa-factorify-plugin and add it to your Japa configuration. The plugin manages the database connection lifecycle, automatically opening and closing it around the test suite. Database configuration follows Knex.js options.
    pnpm add -D @julr/japa-factorify-plugin
    // bin/test.ts
    import { factorify } from '@julr/japa-factorify-plugin'
    
    configure({
      ...processCliArgs(process.argv.slice(2)),
      ...{
        plugins: [
          factorify({
            database: {
              // Knex configuration options
              connection: {
                host: 'localhost',
                user: 'root',
                password: 'password',
                database: 'factorify',
              }
            },
          }),
        ],
      },
    })
  5. Initialize Factorify with database configuration

    Factorify must be initialized using defineFactorifyConfig before creating any models. This initialization should occur in your test runner's setup files or hooks. The defineFactorifyConfig function returns a disconnect function that should be called after tests are complete to close the database connection (e.g., within a Jest afterAll hook). Database configuration options follow the Knex.js configuration standard.
    import { defineFactorifyConfig } from '@julr/factorify'
    
    // Initialize before tests run
    const disconnect = defineFactorifyConfig({
      database: {
        // See https://knexjs.org/guide/#configuration-options
        client: 'sqlite3',
        connection: {
          host: 'localhost',
          user: 'root',
          password: 'password',
          database: 'factorify',
        }
      },
    })
    
    // Close connection after tests (e.g., in Jest)
    afterAll(() => disconnect())
  6. Install @julr/factorify via npm

    Install the @julr/factorify package as a development dependency using your preferred package manager.
    pnpm add -D @julr/factorify
    
    npm install --save-dev @julr/factorify
    
    yarn add -D @julr/factorify
  7. Define a basic factory with Factorify

    Use defineFactory to create a blueprint for generating model data. The first parameter must be the table name of the model. You can pass a generic type to defineFactory for attribute autocompletion. The factory callback provides a faker object (a Faker.js instance) to generate random data. Ensure the returned object contains all required database properties to avoid 'not null' exceptions. Fields can be defined as static values, faker calls, or functions for computed values.
    import type { User } from './types.js'
    
    const UserFactory = defineFactory<User>('user', ({ faker, isStubbed }) => ({
      email: faker.internet.email(), 
      password: faker.random.alphaNumeric(6),
      computedField: () => {
        // You can also use a function to define a field
        return 'computed value'
      }
    }))
      .build()
  8. Override factory attributes with merge

    The .merge() method allows you to override the default attributes of a factory. When using createMany, you can pass an array of attribute objects; the factory will merge these based on their index. If the merge array is shorter than the number of records being created, the remaining records will use the default attributes.
    // Override attributes for a single instance
    await UserFactory
      .merge({ email: 'user@example.com' })
      .create()
    
    // Override attributes for multiple instances by index
    await UserFactory
      .merge([
        { email: 'user1@example.com' },
        { email: 'user2@example.com' },
      ])
      .createMany(3) // First two use merged emails, third uses default
  9. Implement inline relationships for automatic association

    Inline relationships ensure that an associated model is always created whenever the parent model is created. This is achieved by calling the associated factory's .create() method within a field function in the parent factory definition.
    const UserFactory = defineFactory<User>('user', ({ faker }) => ({
      email: faker.internet.email(), 
      password: faker.random.alphaNumeric(6),
    })).build()
    
    const AccountFactory = defineFactory<Account>('account', ({ faker }) => ({
      name: faker.company.companyName(),
      userId: () => UserFactory.create()
    })).build()
    
    // Creating an account now automatically creates and associates a user
    const account = await AccountFactory.create()
  10. Use @julr/japa-database-plugin for database assertions and resets

    The @julr/japa-database-plugin extends Japa with database-specific matchers and utilities. It provides DatabaseUtils.refreshDatabase() to reset the database state between tests and adds assertion methods like assertCount and assertHas to the database object in tests.
    import { test } from '@japa/runner'
    import { DatabaseUtils } from '@julr/japa-database-plugin'
    
    test.group('My group', group => {
      // Refresh the database between each test
      group.each.setup(() => DatabaseUtils.refreshDatabase()) 
    
      test('Should return user', async ({ database, client }) => {
        const user = await UserFactory.merge({ email: 'test@example.com' }).create()
    
        const response = await client.get('/users')
        
        response.assertStatus(200)
        response.assertBody([user])
    
        // Database assertions
        await database.assertCount('users', 1)
        await database.assertHas('users', { email: 'test@example.com' })
      })
    })
    
  11. Create factory states for model variations

    Factory states allow you to define reusable variations of a model. Use the .state() method to define a named variation that overrides specific attributes. You can then apply this state using .apply('stateName') before calling .create().
    const UserFactory = defineFactory<User>('user', ({ faker }) => ({
      email: faker.internet.email(), 
      password: faker.random.alphaNumeric(6),
      role: 'user'
    }))
      .state('admin', () => ({ role: 'admin' }))
      .build()
    
    // Usage: Create an admin user
    const admin = await UserFactory.apply('admin').create()
  12. Apply predefined factory states

    Use the .apply() method to trigger a predefined state on a factory before creating the records.
    // Create 3 records with the 'published' state applied
    await PostFactory.apply('published').createMany(3)