Announcing Fishery – a JavaScript and TypeScript Factory Library

Stephen Hanson

I’m pleased to announce the release of Fishery, a library for setting up JavaScript objects for use in tests and anywhere else you need to set up data. Fishery is influenced by our popular Ruby factory library, factory_bot.

Fishery is built with TypeScript in mind. Factories, which build objects, accept typed parameters and return typed objects, so you can be confident that the data used in your tests is valid. If you aren’t using TypeScript, that’s fine too. Fishery still works, just without the extra type-checking that comes with TypeScript.

Why factories?

Any test suite needs to have the ability to create objects that simulate the objects used by your app. For example, if you have a function called sendNotification(user) or a React component <UserAvatar user={user} />, you need a user object in order to test it.

Manually creating this data in each of your tests is not ideal for several reasons. First, it creates unnecessary code duplication. Each test that uses your objects shouldn’t have to know how to build them. Keeping the logic for how to build your objects in one place makes it easier to maintain your code as your data changes over time.

Another benefit of factories is that they keep irrelevant noise out of your test suite. For example, the following test includes data that might be necessary for your test to work or compile but is not directly relevant to what is being tested — much of it is just noise:

const user = {
  id: 1,
  firstName: "Sonia",
  lastName: "Gutierrez",
  email: "sonia@example.com",
  phone: "123-555-5555",
  avatarUrl: "https://example.com/avatar.png",
}

expect(getFullName(user)).toEqual("Sonia Gutierrez")

Using factories, you define your factory in one central place and then only need to specify the information that is necessary for the test when using the factory. This makes your tests easier to read and maintain:

const user = factories.user.build({
  firstName: "Sonia",
  lastName: "Gutierrez",
}

expect(getFullName(user)).toEqual("Sonia Gutierrez")

You could argue in this contrived example that getFullName wouldn’t need a full User object. Real-world examples are usually more complex, however. Your function might require a full object because it is typed that way (if using TypeScript) or because it accesses other properties that aren’t directly under test.

Installation

First, install fishery with:

npm install --save fishery

or

yarn add fishery

Basic usage

To use Fishery, define your factories and then call build() on them whenever you need to build your object. A factory is just a function that returns your object. Fishery provides several arguments to your factory function to help with common situations. Here’s a quick example:

Define factories

// factories/user.ts
import { Factory } from 'fishery';
import { User } from '../my-types';

export default Factory.define<User>(({ sequence }) => ({
  id: sequence,
  name: 'Bob',
  address: {
    city: 'Austin',
    state: 'TX',
  },
}));

Use factories

Now you can use your factory to build objects:

// my-test.test.ts
import { factories } from './factories';

const user = factories.user.build({ 
  name: 'Susan', 
  address: { city: 'El Paso' } 
});

user.id // 1 (autoincrementing sequence)
user.address.city // El Paso (argument passed to build)
user.address.state // TX (default defined in factory)

Check out the documentation for more information on defining factories and building objects.

Typechecking

Factories are fully typed, both when defining your factories and when using them to build objects, so you can be confident the data you are working with is correct.

The return value of build is type-checked:

const user = factories.user.build();
user.age; // type error! Property 'age' does not exist on type 'User'

The arguments you pass when calling build on your factory are type-checked:

const user = factories.user.build({ age: 10 }); // type error! Argument of type '{ age: number; }' is not assignable to parameter of type 'Partial<User>'.

The arguments that your factory function uses are also typed. Here’s a complex example:

export default Factory.define<User, Factories, UserTransientParams>(
  ({ sequence, params, transientParams, associations, afterCreate }) => {
    params.age; // Property 'age' does not exist on type 'DeepPartial<User>
    transientParams.isAdmin; // Property 'isAdmin' does not exist on type 'Partial<UserTransientParams>'
    associations.posts; // Property 'posts' does not exist on type 'Partial<User>'

    afterCreate(user => {
      user.age; // Property 'age' does not exist on type 'User'
    });

    return {
      id: `user-${sequence}`,
      name: 'Bob',
      post: null,
    };
  },
);

Looking for feedback

Check out the project and let us know what you think. It is still in its early stages, so we are looking for feedback. Contact us on Twitter or open a GitHub issue if you have thoughts on how we could make it better. Happy testing!