factory-builder

An easy way to create framework agnostic scalable test factories

Usage no npm install needed!

<script type="module">
  import factoryBuilder from 'https://cdn.skypack.dev/factory-builder';
</script>

README

Factory Builder

GitHub license NPM Version downloads Test package PRs Welcome

Factory Builder is a framework agnostic and scalable fixtures replacement for your test suite. It has a straightforward definition syntax, mimics multiple build strategies (unsaved instances and attribute hashes), and it allows you to create multiple factories (variants) for the same instance.

It's heavily inspired by FactoryBot by Thoughtbot.

Table of Contents

Installation

This module is distributed via npm which is bundled with node and should be installed as one of your project's devDependencies:

// When using NPM
npm install --save-dev factory-builder

// Or when using Yarn
yarn add --dev factory-builder

Examples

Defining factories

Each factory has a name and a set of attributes. It is highly recommended that you have one factory for each class that provides the simplest set of attributes necessary to create an instance of that class.

Note that the factory is an Object with the attributes key. The value of this attributes key can either be a function that returns an Object or an Object.

// ./factories/User.js
const User = {
  attributes: {
    firstName: 'Thom',
    lastName: 'Taylor',
    email: 'example@example.org',
  }
}

export default User;

Lazily evaluate the attributes

Most of the time, defining the attributes as a object on the factory will be enough. However, the attributes can also be defined as a function. This is useful for when one wants to assign a dynamic value to any of the attributes.

// ./factories/User.js
import { v4 as uuid } from 'uuid'; // NOTE: The `uuid` library is not part of Factory Builder.

const User = {
  attributes: () => ({
    id: uuid(),
    firstName: 'Thom',
    lastName: 'Taylor',
    email: 'example@example.org',
  })
}

export default User;

Consuming factories

On it's own there is nothing special about factories. They're just plain javascript object that return a specifically styled object. To actually use the factory in your test you can make use of multiple methods that are provided by Factory Buider;

  • build: builds a single, new factory.
  • buildList: builds a given number of factories.
  • attributesFor: returns the attributes of a factory.

Building a new factory with build.

Factory Builder doesn't depend on any database, which makes it easy to quickly run your entire test suite. The build simple creates a new factory.

// ./specs/User.js
import { build } from 'factory-builder';
import User from './factories/User';

describe('User', () => {
  it('builds a new user', () => {
    const user = build(User, { lastName: 'build' });
    expect(user.lastName).toEqual('build');
    expect(user.id).toBeUndefined();
  });
});

Building multiple factories with buildList.

You can also create multiple factories at the same time. For this you can use the buildList functions that the Factory Builder package provides.

// ./specs/User.js
import { buildList } from 'factory-builder';
import User from './factories/User';

describe('User', () => {
  it('builds 2 new users', () => {
    const users = buildList(User, 2, { lastName: 'build' });
    expect(users.length).toEqual(2);
  });
});

Building multiple factories with different attributes

In addition to passing an object with attributes, it's also possible to pass an array to buildList in order to use different values for multiple factories.

// ./specs/User.js
import { buildList } from 'factory-builder';
import User from './factories/User';

describe('User', () => {
  it('builds 2 new users with different values', () => {
    const users = buildList(User, 2, [{ lastName: 'build' }, {Β lastName: 'something' }]);
    expect(users.length).toEqual(2);
    expect(users[0].lastName).toEqual('build');
    expect(users[1].lastName).toEqual('something');
  });
});

Before and after hooks

It's also possible to use one of the hooks that Factory Builder provides for injecting some code;

  • beforeBuild - called before building the factory
  • afterBuild - called after building the factory

These methods can be globally defined on your factory or can be passed to the build method. These hooks enable you to modify or use create data to do whatever you want. The before* and after* methods should always return an Object and they always have one argument: the attributes of the factory.

// ./factories/User.js
const User = {
  attributes: {
    firstName: 'Peter',
  },

  beforeBuild: (attributes) => {
    return {
      ...attributes,
      lastName: `${attributes.firstName}s`,
    };
  },

  afterBuild: (attributes) => {
    return {
      ...attributes,
      email: `${attributes.firstName}@email.org`,
    };
  },
};

export default User;

NOTE: It's currently not possible to pass the beforeBuild and afterBuild to either the build or buildList method. These hooks should be defined on the Factory itself.

Using variants

Variants enable you to define multiple variants of the same base factory. You can use the as key when you create or build a factory and it will return the variant you've defined.

const User = {
  attributes: {
    firstName: 'Peter',
    isClient: false,
    isAdmin: false,
  },
  variants: {
    admin: {
      isClient: false,
      isAdmin: true,
    },
    client: {
      isClient: true,
    }
  }
};

import { build } from 'factory-builder';
build(User, as: 'admin'); // => { firstName: 'Peter', isClient: false, isAdmin: true };

NOTE: The variant must be a plain object and should be namespaced in your factory under variants. The key name will also be the way you pick this specific factory variant.

The attributes of a specific variant can also be lazily evaluated. Just like the regular attributes of a Factory. To make use of the lazy evaluation, use a function that returns an object.

import { v4 as uuid } from 'uuid'; // NOTE: The `uuid` library is not part of Factory Builder.

const User = {
  attributes: {
    firstName: 'Peter',
  },

  variants: {
    created: () => ({ id: uuid() })
  }
};

import { build } from 'factory-builder';
build(User, as: 'created'); // => { firstName: 'Peter', id: '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d' };

Quickly grab the attributes with attributesFor.

Want to quickly get all the attributes for a factory (for example to use it as POST data in your tests), you can use the attributesFor method. This will return an object with all the attributes of a factory.

// ./specs/User.js
import { attributesFor } from 'factory-builder';
import User from './factories/User';

describe('User', () => {
  it('sends an API request', () => {
    const userData = attributesFor(User, { lastName: 'Moses' });

    const response = FakeApi('/some-url', {
      method: 'POST',
      body: JSON.stringify(userData),
    });

    expect(response.lastName).toEqual('Moses');
  });
});

NOTE: The attributesFor don't call the beforeBuild and afterBuild hooks for the factory.

Generating data (optional)

You could use a third party library like fakerjs to create fake data for your factories or just define it yourself.

Issues

Looking to contribute? Look for the Good First Issue label.

πŸ› Bugs

Please file an issue for bugs, missing documentation, or unexpected behavior.

See Bugs

πŸ’‘ Feature Requests

Please file an issue to suggest new features. Vote on feature requests by adding a πŸ‘. This helps maintainers prioritize what to work on.

See Feature Requests

❓ Questions

For questions related to using the library, please file an issue on GitHub with the Question label.