A library for setting up JavaScript factories to help build objects as test data, with full TypeScript support
npm install @farcaster/fishery
Fishery is a library for setting up JavaScript objects for use in tests and
anywhere else you need to set up data. It is loosely modeled after the Ruby
gem, [factory_bot][factory_bot].
Fishery is built with TypeScript in mind. Factories 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 typechecking that comes with TypeScript.
Install fishery with:
```
npm install --save-dev fishery
or
``
yarn add --dev fishery
A factory is just a function that returns your object. Fishery provides
several arguments to your factory function to help with common situations.
After defining your factory, you can then call build() on it to build your
objects. Here's how it's done:
`typescript
// factories/user.ts
import { Factory } from 'fishery';
import { User } from '../my-types';
import postFactory from './post';
const userFactory = Factory.define
id: sequence,
name: 'Rosa',
address: { city: 'Austin', state: 'TX', country: 'USA' },
posts: postFactory.buildList(2),
}));
const user = userFactory.build({
name: 'Susan',
address: { city: 'El Paso' },
});
user.name; // Susan
user.address.city; // El Paso
user.address.state; // TX (from factory)
`
In some cases, you might want to perform an asynchronous operation when building objects, such as saving an object to the database. This can be done by calling create instead of build. First, define an onCreate for your factory that specifies the behavior of create, then create objects with create in the same way you do with build:
`typescript
const userFactory = Factory.define
onCreate(user => User.create(user));
return {
...
};
});
const user = await userFactory.create({ name: 'Maria' });
user.name; // Maria
`
create returns a promise instead of the object itself but otherwise has the same API as build. The action that occurs when calling create is specified by defining an onCreate method on your factory as described below.
create can also return a different type from build. This type can be specified when defining your factory:
``
Factory.define
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.
`typescript`
const user = userFactory.build();
user.foo; // type error! Property 'foo' does not exist on type 'User'
`typescript`
const user = userFactory.build({ foo: 'bar' }); // type error! Argument of type '{ foo: string; }' is not assignable to parameter of type 'Partial
`typescript
const userFactory = Factory.define
({
sequence,
params,
transientParams,
associations,
afterBuild,
onCreate,
}) => {
params.firstName; // Property 'firstName' does not exist on type 'DeepPartial
transientParams.foo; // Property 'foo' does not exist on type 'Partial
associations.bar; // Property 'bar' does not exist on type 'Partial
afterBuild(user => {
user.foo; // Property 'foo' does not exist on type 'User'
});
return {
id: user-${sequence},`
name: 'Bob',
post: null,
};
},
);
build supports a second argument with the following keys:
- transient: data for use in your factory that doesn't get overlaid onto yourassociations
result object. More on this in the Transient
Params section
- : often not required but can be useful in order to short-circuit creating associations. More on this in the Associations
section
The parameters passed in to build are automatically overlaid on top of the
default properties defined by your factory, so it is often not necessary to
explicitly access the params in your factory. This can, however, be useful,
for example, if your factory uses the params to compute other properties:
`typescript${kebabCase(name)}@example.com
const userFactory = Factory.define
const { name = 'Bob Smith' } = params;
const email = params.email || ;
return {
name,
email,
posts: [],
};
});
`
Factories can accept parameters that are not part of the resulting object. We
call these transient params. When building an object, pass any transient
params in the second argument:
`typescript`
const user = factories.user.build({}, { transient: { registered: true } });
Transient params are passed in to your factory and can then be used
however you like:
`typescript
type User = {
name: string;
posts: Post[];
memberId: string | null;
permissions: { canPost: boolean };
};
type UserTransientParams = {
registered: boolean;
numPosts: number;
};
const userFactory = Factory.define
({ transientParams, sequence }) => {
const { registered, numPosts = 1 } = transientParams;
const user = {
name: 'Susan Velasquez',
posts: postFactory.buildList(numPosts),
memberId: registered ? member-${sequence} : null,
permissions: {
canPost: registered,
},
};
return user;
},
);
`
In the example above, we also created a type called UserTransientParams anddefine
passed it as the second generic type to . This gives you typebuild
checking of transient params, both in the factory and when calling .
When constructing objects, any regular params you pass to build take
precedence over the transient params:
`typescript
const user = userFactory.build(
{ memberId: '1' },
{ transient: { registered: true } },
);
user.memberId; // '1'
user.permissions.canPost; // true
`
Passing transient params to build can be a bit verbose. It is often a good
idea to consider creating a reusable builder method instead of or in
addition to your transient params to make building objects simpler.
You can instruct factories to execute some code after an object is built.
This can be useful if a reference to the object is needed, like when setting
up relationships:
`typescript
const userFactory = Factory.define
afterBuild(user => {
const post = factories.post.build({}, { associations: { author: user } });
user.posts.push(post);
});
return {
id: sequence,
name: 'Bob',
posts: [],
};
});
`
Similar to onCreate, afterCreates can also be defined. These are executed after the onCreate, and multiple can be defined for a given factory.
`typescript
const userFactory = Factory.define
({ sequence, onCreate, afterCreate }) => {
onCreate(user => apiService.create(user));
afterCreate(savedUser => doMoreStuff(savedUser));
return {
id: sequence,
name: 'Bob',
posts: [],
};
},
);
// can define additional afterCreates
const savedUser = userFactory
.afterCreate(async savedUser => savedUser)
.create();
`
Factories can be extended using the extension methods: params, transient,associations, afterBuild, afterCreate and onCreate. These set defaultbuild
attributes that get passed to the factory on . They return a new factory
and do not modify the factory they are called on :
`typescript
const userFactory = Factory.define
admin: false,
}));
const adminFactory = userFactory.params({ admin: true });
adminFactory.build().admin; // true
userFactory.build().admin; // false
`
params, associations, and transient behave in the same way as the arguments to build. The following are equivalent:
`typescript
const user = userFactory
.params({ admin: true })
.associations({ post: postFactory.build() })
.transient({ name: 'Jared' })
.build();
const user2 = userFactory.build(
{ admin: true },
{
associations: { post: postFactory.build() },
transient: { name: 'Jared' },
},
);
`
Additionally, the following extension methods are available:
- afterBuild - executed after an object is built. Multiple can be definedonCreate
- - defines or replaces the behavior of create(). Must be defined prior to calling create(). Only one can be defined.afterCreate
- - called after onCreate() before the object is returned from create(). Multiple can be defined
These extension methods can be called multiple times to continue extending
factories:
`typescript
const sallyFactory = userFactory
.params({ admin: true })
.params({ name: 'Sally' })
.afterBuild(user => console.log('hello'))
.afterBuild(user => console.log('there'));
const user = sallyFactory.build();
// log: hello
// log: there
user.name; // Sally
user.admin; // true
const user2 = sallyFactory.build({ admin: false });
user.name; // Sally
user2.admin; // false
`
If you find yourself frequently building objects with a certain set of
properties, it might be time to either extend the factory or create a
reusable builder method.
Factories are just classes, so adding reusable builder methods can be achieved by subclassing Factory and defining any desired methods:
`typescriptadmin-${this.sequence()}
class UserFactory extends Factory
admin(adminId?: string) {
return this.params({
admin: true,
adminId: adminId || ,
});
}
registered() {
return this
.params({ memberId: this.sequence() })
.transient({ registered: true })
.associations({ profile: profileFactory.build() })
.afterBuild(user => console.log(user))
}
}
// instead of Factory.define
const userFactory = UserFactory.define(() => ({ ... }))
const user = userFactory.admin().registered().build()
`
To learn more about the factory builder methods params, transient,associations, afterBuild, onCreate, and afterCreate, see Extending factories, above.
Factories can import and reference other factories for associations:
`typescript
import userFactory from './user';
const postFactory = Factory.define
title: 'My Blog Post',
author: userFactory.build(),
}));
`
If you'd like to be able to pass in an association when building your object and
short-circuit the call to yourFactory.build(), use the associations
variable provided to your factory:
`typescript`
const postFactory = Factory.define
title: 'My Blog Post',
author: associations.author || userFactory.build(),
}));
Then build your object like this:
`typescript`
const jordan = userFactory.build({ name: 'Jordan' });
factories.post.build({}, { associations: { author: jordan } });
If two factories reference each other, they can usually import each other
without issues, but TypeScript might require you to explicitly type your
factory before exporting so it can determine the type before the circular
references resolve:
`typescript`
// the extra Factory
const postFactory: Factory
export default postFactory;
A factory's sequence can be rewound with rewindSequence()`.
This sets the sequence back to its original starting value.
See the [CONTRIBUTING] document.
Thank you, [contributors]!
[contributing]: CONTRIBUTING.md
[contributors]: https://github.com/thoughtbot/fishery/graphs/contributors
This project name was inspired by Patrick Rothfuss' _Kingkiller Chronicles_
books. In the books, the artificery, or workshop, is called the Fishery for
short. The Fishery is where things are built.
Fishery is Copyright © 2021 Stephen Hanson and thoughtbot. It is free
software, and may be redistributed under the terms specified in the
LICENSE file.
Fishery is maintained and funded by thoughtbot, inc.
The names and logos for thoughtbot are trademarks of thoughtbot, inc.
We love open source software!
See [our other projects][community] or
[hire us][hire] to design, develop, and grow your product.
[community]: https://thoughtbot.com/community?utm_source=github
[hire]: https://thoughtbot.com/hire-us?utm_source=github
[factory_bot]: https://github.com/thoughtbot/factory_bot