npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@hikoala/kishery

v2.3.4

Published

A library for setting up JavaScript factories to help build objects as test data, with full TypeScript support

Downloads

115

Readme

kishery

CircleCI

kishery 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.

kishery 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 – kishery still works, just without the extra typechecking that comes with TypeScript.

Installation

Install kishery with:

npm install --save-dev @hikoala/kishery

or

yarn add --dev @hikoala/kishery

Usage

A factory is just a function that returns your object. kishery 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

// factories/user.ts
import { Factory } from '@hikoala/kishery';
import { User } from '../my-types';
import postFactory from './post';

const userFactory = Factory.define<User>(({ sequence }) => ({
  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)

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:

const userFactory = Factory.define<User>(({ onCreate }) => {
  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<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.

const user = userFactory.build();
user.foo; // type error! Property 'foo' does not exist on type 'User'
const user = userFactory.build({ foo: 'bar' }); // type error! Argument of type '{ foo: string; }' is not assignable to parameter of type 'Partial<User>'.
const userFactory = Factory.define<User, UserTransientParams>(
  ({
    sequence,
    params,
    transientParams,
    associations,
    afterBuild,
    onCreate,
  }) => {
    params.firstName; // Property 'firstName' does not exist on type 'DeepPartial<User>
    transientParams.foo; // Property 'foo' does not exist on type 'Partial<UserTransientParams>'
    associations.bar; // Property 'bar' does not exist on type 'Partial<User>'

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

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

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 section
  • associations: 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:

const userFactory = Factory.define<User>(({ params }) => {
  const { name = 'Bob Smith' } = params;
  const email = params.email || `${kebabCase(name)}@example.com`;

  return {
    name,
    email,
    posts: [],
  };
});

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:

const user = factories.user.build({}, { transient: { registered: true } });

Transient params are passed in to your factory and can then be used however you like:

type User = {
  name: string;
  posts: Post[];
  memberId: string | null;
  permissions: { canPost: boolean };
};

type UserTransientParams = {
  registered: boolean;
  numPosts: number;
};

const userFactory = Factory.define<User, UserTransientParams>(
  ({ 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 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:

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.

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:

const userFactory = Factory.define<User>(({ sequence, afterBuild }) => {
  afterBuild(user => {
    const post = factories.post.build({}, { associations: { author: user } });
    user.posts.push(post);
  });

  return {
    id: sequence,
    name: 'Bob',
    posts: [],
  };
});

After-create hook

Similar to onCreate, afterCreates can also be defined. These are executed after the onCreate, and multiple can be defined for a given factory.

const userFactory = Factory.define<User, {}, SavedUser>(
  ({ 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();

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 :

const userFactory = Factory.define<User>(() => ({
  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:

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 defined
  • onCreate - 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:

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

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:

class UserFactory extends Factory<User, UserTransientParams> {
  admin(adminId?: string) {
    return this.params({
      admin: true,
      adminId: adminId || `admin-${this.sequence()}`,
    });
  }

  registered() {
    return this
      .params({ memberId: this.sequence() })
      .transient({ registered: true })
      .associations({ profile: profileFactory.build() })
      .afterBuild(user => console.log(user))
  }
}

// instead of Factory.define<User>
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.

Advanced

Associations

Factories can import and reference other factories for associations:

import userFactory from './user';

const postFactory = Factory.define<Post>(() => ({
  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:

const postFactory = Factory.define<Post>(({ associations }) => ({
  title: 'My Blog Post',
  author: associations.author || userFactory.build(),
}));

Then build your object like this:

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:

// the extra Factory<Post> typing can be necessary with circular imports
const postFactory: Factory<Post> = Factory.define<Post>(() => ({ ...}));
export default postFactory;

Rewind Sequence

A factory's sequence can be rewound with rewindSequence(). This sets the sequence back to its original starting value.

Credits

This project name was inspired by Patrick Rothfuss' Kingkiller Chronicles books. In the books, the artificery, or workshop, is called the kishery for short. The kishery is where things are built.

License

It is free software, and may be redistributed under the terms specified in the LICENSE file.

About thoughtbot (original authors)

kishery 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 or hire us to design, develop, and grow your product.