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.
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.
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
Install factory-girl-ts
using npm:
npm install 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 optionaloverride
parameterbuildMany(override?)
: builds an array of the target objectasync create(override)
: creates an instance of the target objectasync 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
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: '[email protected]',
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: '[email protected]', state: 'Some state', country: 'Some country' }
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: '[email protected]', state: 'Some state', country: 'Some country' }
const defaultUser2 = userFactory.build();
console.log(defaultUser2);
// Output: { name: 'John', email: '[email protected]', state: 'Some state', country: 'Some country' }
const defaultUser3 = userFactory.build();
console.log(defaultUser3);
// Output: { name: 'John', email: '[email protected]', state: 'Some state', country: 'Some country' }
You can override default properties when creating a model instance:
const userWithCustomName = userFactory.build({ name: 'Jane' });
console.log(userWithCustomName);
// Output: { name: 'Jane', email: '[email protected]', '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' }
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: '[email protected]',
});
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: '[email protected]' }, { name: 'John', email: '[email protected]' } ]
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'
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.
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: '[email protected]',
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'
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: '[email protected]',
});
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,
});
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: '[email protected]',
}));
const user = companyEmailUser.build();
console.log(user.email); // Output: 'user@company'
The strategy above is helpful to create factories to abstract common use cases.
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.
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!