Installations
npm install @farcaster/fishery
Developer
thoughtbot
Developer Guide
Module System
CommonJS
Min. Node Version
Typescript Support
Yes
Node Version
NPM Version
Statistics
876 Stars
128 Commits
36 Forks
10 Watching
10 Branches
22 Contributors
Updated on 15 Nov 2024
Languages
TypeScript (97.25%)
JavaScript (2.75%)
Total Downloads
Cumulative downloads
Total Downloads
135,651
Last day
27.1%
108
Compared to previous day
Last week
-13.3%
814
Compared to previous week
Last month
-18.8%
3,415
Compared to previous month
Last year
38.1%
75,000
Compared to previous year
Daily Downloads
Weekly Downloads
Monthly Downloads
Yearly Downloads
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.
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.
Installation
Install fishery with:
npm install --save-dev fishery
or
yarn add --dev fishery
Usage
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:
Define and use factories
1// factories/user.ts 2import { Factory } from 'fishery'; 3import { User } from '../my-types'; 4import postFactory from './post'; 5 6const userFactory = Factory.define<User>(({ sequence }) => ({ 7 id: sequence, 8 name: 'Rosa', 9 address: { city: 'Austin', state: 'TX', country: 'USA' }, 10 posts: postFactory.buildList(2), 11})); 12 13const user = userFactory.build({ 14 name: 'Susan', 15 address: { city: 'El Paso' }, 16}); 17 18user.name; // Susan 19user.address.city; // El Paso 20user.address.state; // TX (from factory)
Asynchronously create objects with your factories
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
:
1const userFactory = Factory.define<User>(({ onCreate }) => { 2 onCreate(user => User.create(user)); 3 4 return { 5 ... 6 }; 7}); 8 9const user = await userFactory.create({ name: 'Maria' }); 10user.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<ReturnTypeOfBuild, TransientParamsType, ReturnTypeOfCreate>
Documentation
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.
1const user = userFactory.build(); 2user.foo; // type error! Property 'foo' does not exist on type 'User'
1const user = userFactory.build({ foo: 'bar' }); // type error! Argument of type '{ foo: string; }' is not assignable to parameter of type 'Partial<User>'.
1const userFactory = Factory.define<User, UserTransientParams>( 2 ({ 3 sequence, 4 params, 5 transientParams, 6 associations, 7 afterBuild, 8 onCreate, 9 }) => { 10 params.firstName; // Property 'firstName' does not exist on type 'DeepPartial<User> 11 transientParams.foo; // Property 'foo' does not exist on type 'Partial<UserTransientParams>' 12 associations.bar; // Property 'bar' does not exist on type 'Partial<User>' 13 14 afterBuild(user => { 15 user.foo; // Property 'foo' does not exist on type 'User' 16 }); 17 18 return { 19 id: `user-${sequence}`, 20 name: 'Bob', 21 post: null, 22 }; 23 }, 24);
build
API
build
supports a second argument with the following keys:
transient
: data for use in your factory that doesn't get overlaid onto your result object. More on this in the Transient Params sectionassociations
: often not required but can be useful in order to short-circuit creating associations. More on this in the Associations section
Use params
to access passed in properties
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:
1const userFactory = Factory.define<User>(({ params }) => { 2 const { name = 'Bob Smith' } = params; 3 const email = params.email || `${kebabCase(name)}@example.com`; 4 5 return { 6 name, 7 email, 8 posts: [], 9 }; 10});
Params that don't map to the result object (transient params)
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:
1const user = factories.user.build({}, { transient: { registered: true } });
Transient params are passed in to your factory and can then be used however you like:
1type User = { 2 name: string; 3 posts: Post[]; 4 memberId: string | null; 5 permissions: { canPost: boolean }; 6}; 7 8type UserTransientParams = { 9 registered: boolean; 10 numPosts: number; 11}; 12 13const userFactory = Factory.define<User, UserTransientParams>( 14 ({ transientParams, sequence }) => { 15 const { registered, numPosts = 1 } = transientParams; 16 17 const user = { 18 name: 'Susan Velasquez', 19 posts: postFactory.buildList(numPosts), 20 memberId: registered ? `member-${sequence}` : null, 21 permissions: { 22 canPost: registered, 23 }, 24 }; 25 26 return user; 27 }, 28);
In the example above, we also created a type called UserTransientParams
and
passed it as the second generic type to define
. This gives you type
checking of transient params, both in the factory and when calling build
.
When constructing objects, any regular params you pass to build
take
precedence over the transient params:
1const user = userFactory.build( 2 { memberId: '1' }, 3 { transient: { registered: true } }, 4); 5 6user.memberId; // '1' 7user.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.
After-build hook
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:
1const userFactory = Factory.define<User>(({ sequence, afterBuild }) => { 2 afterBuild(user => { 3 const post = factories.post.build({}, { associations: { author: user } }); 4 user.posts.push(post); 5 }); 6 7 return { 8 id: sequence, 9 name: 'Bob', 10 posts: [], 11 }; 12});
After-create hook
Similar to onCreate
, afterCreate
s can also be defined. These are executed after the onCreate
, and multiple can be defined for a given factory.
1const userFactory = Factory.define<User, {}, SavedUser>( 2 ({ sequence, onCreate, afterCreate }) => { 3 onCreate(user => apiService.create(user)); 4 afterCreate(savedUser => doMoreStuff(savedUser)); 5 6 return { 7 id: sequence, 8 name: 'Bob', 9 posts: [], 10 }; 11 }, 12); 13 14// can define additional afterCreates 15const savedUser = userFactory 16 .afterCreate(async savedUser => savedUser) 17 .create();
Extending factories
Factories can be extended using the extension methods: params
, transient
,
associations
, afterBuild
, afterCreate
and onCreate
. These set default
attributes that get passed to the factory on build
. They return a new factory
and do not modify the factory they are called on :
1const userFactory = Factory.define<User>(() => ({ 2 admin: false, 3})); 4 5const adminFactory = userFactory.params({ admin: true }); 6adminFactory.build().admin; // true 7userFactory.build().admin; // false
params
, associations
, and transient
behave in the same way as the arguments to build
. The following are equivalent:
1const user = userFactory 2 .params({ admin: true }) 3 .associations({ post: postFactory.build() }) 4 .transient({ name: 'Jared' }) 5 .build(); 6 7const user2 = userFactory.build( 8 { admin: true }, 9 { 10 associations: { post: postFactory.build() }, 11 transient: { name: 'Jared' }, 12 }, 13);
Additionally, the following extension methods are available:
afterBuild
- executed after an object is built. Multiple can be definedonCreate
- defines or replaces the behavior ofcreate()
. Must be defined prior to callingcreate()
. Only one can be defined.afterCreate
- called afteronCreate()
before the object is returned fromcreate()
. Multiple can be defined
These extension methods can be called multiple times to continue extending factories:
1const sallyFactory = userFactory 2 .params({ admin: true }) 3 .params({ name: 'Sally' }) 4 .afterBuild(user => console.log('hello')) 5 .afterBuild(user => console.log('there')); 6 7const user = sallyFactory.build(); 8// log: hello 9// log: there 10user.name; // Sally 11user.admin; // true 12 13const user2 = sallyFactory.build({ admin: false }); 14user.name; // Sally 15user2.admin; // false
Adding reusable builders (traits) to factories
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:
1class UserFactory extends Factory<User, UserTransientParams> { 2 admin(adminId?: string) { 3 return this.params({ 4 admin: true, 5 adminId: adminId || `admin-${this.sequence()}`, 6 }); 7 } 8 9 registered() { 10 return this 11 .params({ memberId: this.sequence() }) 12 .transient({ registered: true }) 13 .associations({ profile: profileFactory.build() }) 14 .afterBuild(user => console.log(user)) 15 } 16} 17 18// instead of Factory.define<User> 19const userFactory = UserFactory.define(() => ({ ... })) 20 21const user = userFactory.admin().registered().build()
To learn more about the factory builder methods params
, transient
,
associations
, afterBuild
, onCreate
, and afterCreate
, see Extending factories, above.
Advanced
Associations
Factories can import and reference other factories for associations:
1import userFactory from './user'; 2 3const postFactory = Factory.define<Post>(() => ({ 4 title: 'My Blog Post', 5 author: userFactory.build(), 6}));
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:
1const postFactory = Factory.define<Post>(({ associations }) => ({ 2 title: 'My Blog Post', 3 author: associations.author || userFactory.build(), 4}));
Then build your object like this:
1const jordan = userFactory.build({ name: 'Jordan' }); 2factories.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:
1// the extra Factory<Post> typing can be necessary with circular imports 2const postFactory: Factory<Post> = Factory.define<Post>(() => ({ ...})); 3export default postFactory;
Rewind Sequence
A factory's sequence can be rewound with rewindSequence()
.
This sets the sequence back to its original starting value.
Contributing
See the CONTRIBUTING document. Thank you, contributors!
Credits
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.
License
Fishery is Copyright © 2021 Stephen Hanson and thoughtbot. It is free software, and may be redistributed under the terms specified in the LICENSE file.
About thoughtbot
This repo 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. We are available for hire.
No vulnerabilities found.
Reason
no dangerous workflow patterns detected
Reason
no binaries found in the repo
Reason
security policy file detected
Details
- Info: security policy file detected: SECURITY.md:1
- Info: Found linked content: SECURITY.md:1
- Info: Found disclosure, vulnerability, and/or timelines in security policy: SECURITY.md:1
- Info: Found text in security policy: SECURITY.md:1
Reason
license file detected
Details
- Info: project has a license file: LICENSE:0
- Info: FSF or OSI recognized license: MIT License: LICENSE:0
Reason
detected GitHub workflow tokens with excessive permissions
Details
- Warn: jobLevel 'contents' permission set to 'write': .github/workflows/dynamic-readme.yml:12
- Warn: jobLevel 'contents' permission set to 'write': .github/workflows/dynamic-security.yml:14
- Warn: no topLevel permission defined: .github/workflows/dynamic-readme.yml:1
- Warn: no topLevel permission defined: .github/workflows/dynamic-security.yml:1
Reason
Found 6/22 approved changesets -- score normalized to 2
Reason
3 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 2
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
project is not fuzzed
Details
- Warn: no fuzzer integrations found
Reason
branch protection not enabled on development/release branches
Details
- Warn: branch protection not enabled for branch 'main'
Reason
SAST tool is not run on all commits -- score normalized to 0
Details
- Warn: 0 commits out of 18 are checked with a SAST tool
Reason
12 existing vulnerabilities detected
Details
- Warn: Project is vulnerable to: GHSA-67hx-6x53-jw92
- Warn: Project is vulnerable to: GHSA-grv7-fg5c-xmjg
- Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275
- Warn: Project is vulnerable to: GHSA-w573-4hg7-7wgq
- Warn: Project is vulnerable to: GHSA-9c47-m6qq-7p4h
- Warn: Project is vulnerable to: GHSA-952p-6rrq-rcjv
- Warn: Project is vulnerable to: GHSA-f8q6-p94x-37v3
- Warn: Project is vulnerable to: GHSA-gcx4-mw62-g8wm
- Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw
- Warn: Project is vulnerable to: GHSA-72xf-g2v4-qvf3
- Warn: Project is vulnerable to: GHSA-j8xg-fqg3-53r7
- Warn: Project is vulnerable to: GHSA-3h5v-q93c-6h6q
Score
4.6
/10
Last Scanned on 2024-11-18
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn MoreOther packages similar to @farcaster/fishery
fishery
A library for setting up JavaScript factories to help build objects as test data, with full TypeScript support
@farcaster/auth-client
A framework agnostic client library for Farcaster Auth.
@farcaster/core
Shared hub-related method and classes.
@standard-crypto/farcaster-js-hub-rest
A tool for interacting with the REST API of any Farcaster hub.