Skip to content

vizeat/factory-girl-ts

 
 

Repository files navigation

Tests

Factory Girl TypeScript (factory-girl-ts)

factory-girl-ts is a modern, easy-to-use library for creating test data in Typescript projects. Drawing inspiration from the factory_bot Ruby gem and the fishery library, factory-girl-ts is designed for seamless integration with popular ORMs like Sequelize and Typeorm.

Why factory-girl-ts?

While factory-girl is a renowned library for creating test data in Node.js, it hasn't been updated since 2018. factory-girl-ts was born to fulfill the need for an updated, TypeScript-compatible library focusing on ease of use, especially when it comes to creating associations and asynchronous operations.

TL;DR

factory-girl-ts is a TypeScript-compatible library created for crafting test data. It is designed to fit smoothly with ORMs such as Sequelize and TypeORM.

Key features of factory-girl-ts include:

  • A Simple and Intuitive API: This library makes the defining and creating of test data simple and quick.
  • Seamless ORM Integration: It has been designed to integrate effortlessly with Sequelize and TypeORM.
  • Built-in Support for Associations: It allows for simple creation of models with associations, making it perfect for complex data structures.
  • Repository and Active Record Pattern Compatibility: Depending on your project's requirements, you can choose the most suitable pattern.

factory-girl-ts uses an instance of the Factory class to define factories. The Factory class offers several methods for building and creating instances of your models. You can create single or multiple instances, with or without custom attributes, and the library also supports creating instances with associations.

It also allows you to specify an adapter for your ORM, and currently supports three adapters: TypeOrmRepositoryAdapter, SequelizeAdapter, and ObjectAdapter.

Here's a simple class diagram showing how the main pieces of the library fit together:

classDiagram
    FactoryGirl --|> Factory : creates
    ModelAdapter <|.. TypeOrmRepositoryAdapter
    ModelAdapter <|.. SequelizeAdapter
    ModelAdapter <|.. ObjectAdapter
    FactoryGirl o-- ModelAdapter : uses
    Factory o-- Factory : associate
    Factory --|> Entity : creates

Loading

Getting Started

Install factory-girl-ts using npm:

npm install factory-girl-ts

How to Use factory-girl-ts

Factories in factory-girl-ts are instances of the Factory class, offering several methods for building and creating instances of your models.

  • build(override?): builds the target object, with an optional override parameter
  • buildMany(override?): builds an array of the target object
  • async create(override): creates an instance of the target object
  • async createMany(override): creates an array of instances of the target object

Let's see how to define a factory and use each of the methods above

Defining a Factory (Sequelize Example)

import { User } from './models/user';
import { FactoryGirl, SequelizeAdapter } from 'factory-girl-ts';

// Step 1: Specify the adapter for your ORM.
FactoryGirl.setAdapter(new SequelizeAdapter());

// Step 2: Define your factory with default attributes for the model.
const defaultAttributesFactory = () => ({
  name: 'John',
  email: 'some-email@mail.com',
  address: {
    state: 'Some state',
    country: 'Some country',
  },
});
const userFactory = FactoryGirl.define(User, defaultAttributesFactory);

// Step 3: Use the factory to create instances of the model.
const defaultUser = userFactory.build();
console.log(defaultUser);
// Output: { name: 'John', email: 'some-email@mail.com', state: 'Some state', country: 'Some country' }

Sequences

Instead of providing a hardcoded value, we can tell factory-girl-ts to instead use a sequence. The first parameter is an unique id. It can be used for sharing sequence across multiple factories. The second parameter is a callback that give you an integer auto-incremented that you can use for construct your value.

import { User } from './models/user';
import { FactoryGirl, SequelizeAdapter } from 'factory-girl-ts';

// Step 1: Specify the adapter for your ORM.
FactoryGirl.setAdapter(new SequelizeAdapter());

// Step 2: Define your factory with default attributes for the model.
const defaultAttributesFactory = () => ({
  name: 'John',
  email: FactoryGirl.sequence<string>(
    'user.email',
    (n: number) => `some-email-${n}@mail.com`,
  ),
  address: {
    state: 'Some state',
    country: 'Some country',
  },
});
const userFactory = FactoryGirl.define(User, defaultAttributesFactory);

// Step 3: Use the factory to create instances of the model.
const defaultUser = userFactory.build();
console.log(defaultUser);
// Output: { name: 'John', email: 'some-email-1@mail.com', state: 'Some state', country: 'Some country' }
const defaultUser2 = userFactory.build();
console.log(defaultUser2);
// Output: { name: 'John', email: 'some-email-2@mail.com', state: 'Some state', country: 'Some country' }
const defaultUser3 = userFactory.build();
console.log(defaultUser3);
// Output: { name: 'John', email: 'some-email-3@mail.com', state: 'Some state', country: 'Some country' }

Overriding Default Properties

You can override default properties when creating a model instance:

const userWithCustomName = userFactory.build({ name: 'Jane' });
console.log(userWithCustomName);
// Output: { name: 'Jane', email: 'some-email@mail.com', 'Some state', country: 'Some country' }

// Overriding nested properties:
const userWithCustomAddress = userFactory.build({
  address: { state: 'Another state' },
});
console.log(userWithCustomAddress);
// Output: { name: 'John', email: 'some-email@mail', state: 'Another state', country: 'Some country' }

Building Multiple Instances with buildMany()

The buildMany() function enables you to create multiple instances of a model at once. Let's walk through an example of how to use it.

import { User } from './models/user';
import { FactoryGirl, SequelizeAdapter } from 'factory-girl-ts';

// 1. Set the adapter for your ORM.
FactoryGirl.setAdapter(new SequelizeAdapter());

// 2. Define your factory with default attributes for the model.
const defaultAttributesFactory = () => ({
  name: 'John',
  email: 'some-email@mail.com',
});
const userFactory = FactoryGirl.define(User, defaultAttributesFactory);

// 3. Create multiple instances of the model.
const users = userFactory.buildMany(2);
console.log(users);
// Output: [ { name: 'John', email: 'some-email@mail.com' }, { name: 'John', email: 'some-email@mail.com' } ]

Overriding Default Attributes for Multiple Instances

buildMany() also allows you to override default attributes for each created instance:

// Create multiple instances with custom attributes.
const [jane, mary] = userFactory.buildMany(
  2,
  { name: 'Jane' },
  { name: 'Mary' },
);
console.log(jane.name); // Output: 'Jane'
console.log(mary.name); // Output: 'Mary'

Applying the Same Override to All Instances

If you want to apply the same override to all instances, you can do that too:

// Create multiple instances with the same custom attribute.
const [user1, user2] = userFactory.buildMany(2, { name: 'Foo' });
console.log(user1.name); // Output: 'Foo'
console.log(user2.name); // Output: 'Foo'

By using buildMany(), you can efficiently create multiple model instances for your tests, with the flexibility to customize their attributes as needed.

Creating Instances with create()

The create() function allows you to create an instance of a model and save it to the database. Let's walk through an example of how to use it.

import { User } from './models/user';
import { FactoryGirl, SequelizeAdapter } from 'factory-girl-ts';

// Step 1: Specify the adapter for your ORM.
FactoryGirl.setAdapter(new SequelizeAdapter());

// Step 2: Define your factory with default attributes for the model.
const defaultAttributesFactory = () => ({
  name: 'John',
  email: 'some-email@mail.com',
  address: {
    state: 'Some state',
    country: 'Some country',
  },
});
const userFactory = FactoryGirl.define(User, defaultAttributesFactory);

// Step 3: Use the factory to create instances of the model.
const defaultUser = await userFactory.create();

// The factory returns a sequelize instance of the given model. Therefore, we can use sequelize's methods:
console.log(defaultUser.get('name')); // Output: 'John'

You can also override default properties and create many instances of the model at once, just like with build() and buildMany():

// Create an instance with custom attributes.
const userWithCustomName = await userFactory.create({ name: 'Jane' });
console.log(userWithCustomName.get('name')); // Output: 'Jane'

// Create multiple instances with custom attributes.
const [jane, mary] = await userFactory.createMany(
  2,
  { name: 'Jane' },
  { name: 'Mary' },
);
console.log(jane.get('name')); // Output: 'Jane'
console.log(mary.get('name')); // Output: 'Mary'

// Create multiple instances with the same custom attribute.
const [user1, user2] = await userFactory.createMany(2, { name: 'Foo' });
console.log(user1.get('name')); // Output: 'Foo'
console.log(user2.get('name')); // Output: 'Foo'

Working with Associations

factory-girl-ts provides an easy way to create associations between models using the associate() method. This method links a model to another by using an attribute from the associated model.

Let's walk through an example to demonstrate how this works. We'll be using a User model and an Address model, where each user has one address.

// Define the User factory.
const defaultAttributesFactory = () => ({
  id: 1,
  name: 'John',
  email: 'some-email@mail.com',
});

const userFactory = FactoryGirl.define(User, defaultAttributesFactory);

// Define the Address factory, associating it with the User factory.
const addressFactory = FactoryGirl.define(Address, () => ({
  id: 1,
  street: '123 Fake St.',
  city: 'Springfield',
  state: 'IL',
  zip: '90210',
  userId: userFactory.associate('id'), // Associates the 'id' from the User model.
}));

const address = addressFactory.build();
const addressInDatabase = await addressFactory.create();

address.get('userId'); // Output: 1
addressInDatabase.get('userId'); // Output: 1

The associate() function coordinates with the method called in the parent factory. If you call build() on the parent factory, associate() will trigger the associated factory's build() method. Conversely, if you call create(), it will invoke the create() method in the associated factory.

Additionally, associate() allows you to specify a custom attribute (or 'key') for associating the models.

// Define the Address factory using a custom 'key' to associate with the User factory.
const addressFactory = FactoryGirl.define(Address, () => ({
  id: 1,
  street: '123 Fake St.',
  city: 'Springfield',
  state: 'IL',
  zip: '90210',
  userId: userFactory.associate('uuid'), // Uses the 'uuid' attribute from the User model for association.
}));

Lastly, the associate() method only comes into play if no value is provided for the given association. This prevents unnecessary creation of entities and can be particularly useful when you want to control the associated value.

// Create an Address instance with a specified 'userId'. This will bypass the 'associate()' method in the User factory.
const addressFromFirstUser = await addressFactory.create({
  userId: 1,
});

Extending Factories

You can extend a factory by using the extend() method. This allows you to create a new factory that inherits the attributes of the parent factory, while also adding new attributes.

const companyEmailUser = userFactory.extend(() => ({
  email: 'user@company.com',
}));

const user = companyEmailUser.build();
console.log(user.email); // Output: 'user@company'

The strategy above is helpful to create factories to abstract common use cases.

Factory Hooks

You can also define hooks to run after creating an instance. This might be handy when there is custom logic or async logic to be executed.

const adminUserFactory = userFactory.afterCreate((user) => {
  const userRole = await userRoleFactory.create({
    userId: user.id,
    role: 'admin',
  });
  user.userRole = userRole;
  return user;
});

The afterCreate() hooks return a brand new factory, so you can chain as many hooks as you want. Moreover, this hook requires that the input model (in the example above, a User) is returned.

Conclusion

In summary, factory-girl-ts allows you to handle model associations seamlessly. The associate() method is a powerful tool that helps you link models together using their attributes, making it easier than ever to create complex data structures for your tests.

Stay tuned for more features and improvements. We are continuously working to make factory-girl-ts the most intuitive and efficient tool for generating test data in TypeScript!

About

A TS version of factory the girl library

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 99.8%
  • Shell 0.2%