fishery

repository·main·Indexed 21 days ago

https://github.com/thoughtbot/fishery

A JavaScript and TypeScript library for setting up object factories to build test data and seed data. Inspired by the Ruby gem factory_bot, it provides a type-safe way to define factories using Factory.define(), supporting synchronous object generation via build(), asynchronous creation via create(), transient parameters, and lifecycle hooks such as afterBuild, onCreate, and afterCreate.

Tokens
4.9K
Snippets
22
Records
25
Agent score
77%

What's inside fishery

  1. Use afterBuild and afterCreate hooks

    main

    Fishery provides hooks to execute code during the object lifecycle:

    • afterBuild: Executed after an object is built. Useful for setting up relationships or modifying the object.
    • afterCreate: Executed after the onCreate logic is finished. Multiple afterCreate hooks can be defined.
    • onCreate: Defines or replaces the behavior of .create(). Only one can be defined.
    // afterBuild example
    const userFactory = Factory.define<User>(({ sequence, afterBuild }) => {
      afterBuild(user => {
        // perform side effects or setup relationships
      });
      return { id: sequence, name: 'Bob' };
    });
    
    // afterCreate example
    const userFactory = Factory.define<User, {}, SavedUser>(({ onCreate, afterCreate }) => {
      onCreate(user => apiService.create(user));
      afterCreate(savedUser => doMoreStuff(savedUser));
    
      return { id: 1, name: 'Bob' };
    });
  2. Use transient params for data that doesn't map to the result object

    main

    Transient params are properties passed to a factory that are used for logic during construction but are not included in the final object. To use them:

    1. Define a type for your transient parameters.
    2. Pass that type as the second generic argument to Factory.define<T, TransientParamsType>.
    3. Access them via the transientParams property in the factory definition.
    4. Pass them to .build() using the second argument with the transient key.

    Regular parameters passed to .build() take precedence over transient parameters.

    type User = { name: string; memberId: string | null; };
    type UserTransientParams = { registered: boolean; };
    
    const userFactory = Factory.define<User, UserTransientParams>(({ transientParams, sequence }) => {
      const { registered } = transientParams;
    
      return {
        name: 'Susan',
        memberId: registered ? `member-${sequence}` : null,
      };
    });
    
    // Usage
    const user = userFactory.build(
      {}, 
      { transient: { registered: true } }
    );
  3. Define associations between factories

    main

    Factories can reference other factories to create associations. You can either call .build() directly within the factory definition or use the associations object to allow overriding the association during the build process.

    To allow passing in an existing object as an association instead of triggering a new build, use the associations property provided in the factory definition function. This is useful for short-circuiting the default build behavior.

    When dealing with circular dependencies (where two factories reference each other), you may need to explicitly type the factory using Factory<T> to help TypeScript resolve the types correctly.

    import userFactory from './user';
    
    // Basic association
    const postFactory = Factory.define<Post>(() => ({
      title: 'My Blog Post',
      author: userFactory.build(),
    }));
    
    // Association that can be overridden via the associations argument
    const postFactoryWithOverride = Factory.define<Post>(({ associations }) => ({
      title: 'My Blog Post',
      author: associations.author || userFactory.build(),
    }));
    
    // Building with an override
    const jordan = userFactory.build({ name: 'Jordan' });
    postFactoryWithOverride.build({}, { associations: { author: jordan } });
    
    // Handling circular imports with explicit typing
    const postFactoryCircular: Factory<Post> = Factory.define<Post>(() => ({ ... }));
  4. Add reusable builder methods (traits) by subclassing Factory

    main

    For complex or frequently used configurations, subclass Factory to add custom, named builder methods (often called 'traits'). This allows for a fluent API like userFactory.admin().registered().build().

    class UserFactory extends Factory<User, UserTransientParams> {
      admin(adminId?: string) {
        return this.params({
          admin: true,
          adminId: adminId || `admin-${this.sequence()}`,
        });
      }
    }
    
    const userFactory = UserFactory.define(() => ({ ... }));
    const user = userFactory.admin().build();
  5. Asynchronously create objects with create()

    main

    To perform asynchronous operations (like saving to a database) when building objects, use the .create() method. You must first define the behavior of create using the onCreate hook inside the Factory.define callback. Note that .create() returns a Promise of the object.

    const userFactory = Factory.define<User>(({ onCreate }) => {
      onCreate(user => User.create(user)); // Define the async behavior
    
      return {
        id: 1,
        name: 'Maria',
        // ...
      };
    });
    
    const user = await userFactory.create({ name: 'Maria' });
    user.name; // Maria
  6. Define and use factories with build()

    main

    A factory is a function that returns an object. Use Factory.define<T> to create a factory. You can then call .build() to generate an object. The .build() method accepts an object of properties to override the defaults. Fishery provides a sequence helper to generate unique values.

    import { Factory } from 'fishery';
    import { User } from '../my-types';
    
    const userFactory = Factory.define<User>(({ sequence }) => ({
      id: sequence,
      name: 'Rosa',
      address: { city: 'Austin', state: 'TX', country: 'USA' },
    }));
    
    const user = userFactory.build({
      name: 'Susan',
      address: { city: 'El Paso' },
    });
    
    user.name; // Susan
    user.address.city; // El Paso
    user.address.state; // TX (from factory)
  7. Use factory hooks for lifecycle management

    main

    Fishery factories support several hooks to manipulate objects during their lifecycle. These hooks can be defined within the factory configuration to handle logic after an object is built or after it is created.

    • afterBuild: A hook that receives a HookFn to run after the object is constructed.
    • onCreate: A hook that receives an OnCreateFn, allowing you to transform the object or perform async operations to return a new version of the object.
    • afterCreate: A hook that receives an AfterCreateFn, typically used for post-creation logic that returns the object (or a promise of it).
  8. Understand the DeepPartial type

    main

    The DeepPartial<T> type is used within Fishery to allow for partial overrides of factory-generated objects. Unlike a standard Partial<T>, which only makes the top-level properties optional, DeepPartial recursively applies partiality to nested objects.

    Behavioral details:

    • Objects: All properties become optional, and their values are also wrapped in DeepPartial.
    • Arrays, Sets, and Maps: These are preserved as-is (not made partial) to maintain structural integrity.
    • Primitives: Handled via Partial<T>.
    • Functions: Become optional (T | undefined).
    • Dates: Preserved as Date objects.
    // Example of how DeepPartial behaves conceptually:
    type User = {
      id: string;
      profile: {
        name: string;
        settings: {
          theme: string;
        };
      };
    };
    
    // DeepPartial<User> allows:
    // { profile: { settings: { theme: 'dark' } } }
    // instead of requiring the full nested structure.
  9. Reset a factory's sequence with rewindSequence()

    main

    If you are using sequences within a factory, you can reset the sequence counter to its original starting value by calling rewindSequence() on the factory instance.

    factory.rewindSequence();
  10. Extend factories with builder methods

    main

    You can create new factory instances with pre-set attributes using extension methods. These methods return a new factory and do not modify the original.

    Available extension methods:

    • .params(attrs): Sets default attributes to be overlaid on the result.
    • .transient(attrs): Sets default transient parameters.
    • .associations(attrs): Sets default associations.
    • .afterBuild(callback): Adds an afterBuild hook.
    • .afterCreate(callback): Adds an afterCreate hook.
    • .onCreate(callback): Defines the create behavior.
    const userFactory = Factory.define<User>(() => ({ admin: false }));
    
    // Returns a new factory instance
    const adminFactory = userFactory.params({ admin: true });
    
    adminFactory.build().admin; // true
    userFactory.build().admin; // false
  11. Configure the return types for create()

    main

    When using .create(), the return type might differ from the type returned by .build(). You can specify these types using the following generic signature in Factory.define:

    Factory.define<ReturnTypeOfBuild, TransientParamsType, ReturnTypeOfCreate>