![Tests](https://github.com/thiagomini/factory-girl-ts/actions/workflows/node.js.yml/badge.svg)
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
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
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
Defining a Factory (Sequelize Example)
import { User } from './models/user';
import { FactoryGirl, SequelizeAdapter } from 'factory-girl-ts';
FactoryGirl.setAdapter(new SequelizeAdapter());
const defaultAttributesFactory = () => ({
name: 'John',
email: 'some-email@mail.com',
address: {
state: 'Some state',
country: 'Some country',
},
});
const userFactory = FactoryGirl.define(User, defaultAttributesFactory);
const defaultUser = userFactory.build();
console.log(defaultUser);
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';
FactoryGirl.setAdapter(new SequelizeAdapter());
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);
const defaultUser = userFactory.build();
console.log(defaultUser);
const defaultUser2 = userFactory.build();
console.log(defaultUser2);
const defaultUser3 = userFactory.build();
console.log(defaultUser3);
Overriding Default Properties
You can override default properties when creating a model instance:
const userWithCustomName = userFactory.build({ name: 'Jane' });
console.log(userWithCustomName);
const userWithCustomAddress = userFactory.build({
address: { state: 'Another state' },
});
console.log(userWithCustomAddress);
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';
FactoryGirl.setAdapter(new SequelizeAdapter());
const defaultAttributesFactory = () => ({
name: 'John',
email: 'some-email@mail.com',
});
const userFactory = FactoryGirl.define(User, defaultAttributesFactory);
const users = userFactory.buildMany(2);
console.log(users);
Overriding Default Attributes for Multiple Instances
buildMany() also allows you to override default attributes for each created instance:
const [jane, mary] = userFactory.buildMany(
2,
{ name: 'Jane' },
{ name: 'Mary' },
);
console.log(jane.name);
console.log(mary.name);
Applying the Same Override to All Instances
If you want to apply the same override to all instances, you can do that too:
const [user1, user2] = userFactory.buildMany(2, { name: 'Foo' });
console.log(user1.name);
console.log(user2.name);
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';
FactoryGirl.setAdapter(new SequelizeAdapter());
const defaultAttributesFactory = () => ({
name: 'John',
email: 'some-email@mail.com',
address: {
state: 'Some state',
country: 'Some country',
},
});
const userFactory = FactoryGirl.define(User, defaultAttributesFactory);
const defaultUser = await userFactory.create();
console.log(defaultUser.get('name'));
You can also override default properties and create many instances of the model at once, just like with build()
and buildMany()
:
const userWithCustomName = await userFactory.create({ name: 'Jane' });
console.log(userWithCustomName.get('name'));
const [jane, mary] = await userFactory.createMany(
2,
{ name: 'Jane' },
{ name: 'Mary' },
);
console.log(jane.get('name'));
console.log(mary.get('name'));
const [user1, user2] = await userFactory.createMany(2, { name: 'Foo' });
console.log(user1.get('name'));
console.log(user2.get('name'));
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.
const defaultAttributesFactory = () => ({
id: 1,
name: 'John',
email: 'some-email@mail.com',
});
const userFactory = FactoryGirl.define(User, defaultAttributesFactory);
const addressFactory = FactoryGirl.define(Address, () => ({
id: 1,
street: '123 Fake St.',
city: 'Springfield',
state: 'IL',
zip: '90210',
userId: userFactory.associate('id'),
}));
const address = addressFactory.build();
const addressInDatabase = await addressFactory.create();
address.get('userId');
addressInDatabase.get('userId');
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.
const addressFactory = FactoryGirl.define(Address, () => ({
id: 1,
street: '123 Fake St.',
city: 'Springfield',
state: 'IL',
zip: '90210',
userId: userFactory.associate('uuid'),
}));
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.
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);
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!