Test Data Factories in JavaScript: Patterns, Libraries, and TypeScript
Test data factories eliminate the boilerplate of constructing test objects.
Test data factories eliminate the boilerplate of constructing test objects. Instead of manually building up every field of every object in every test, you define a factory once — with sensible defaults — and tests only specify what matters.
This guide covers factory patterns in JavaScript from scratch, then looks at dedicated libraries for larger projects.
The Factory Pattern
At its simplest, a factory is a function with defaults that accepts overrides:
function createUser(overrides = {}) {
return {
id: crypto.randomUUID(),
email: 'user@example.com',
name: 'Test User',
role: 'user',
isActive: true,
createdAt: new Date(),
...overrides,
};
}A test for role-based access only specifies role:
test('admin can access user management', () => {
const admin = createUser({ role: 'admin' });
expect(hasAccess(admin, '/admin/users')).toBe(true);
});
test('regular user cannot access user management', () => {
const user = createUser(); // role defaults to 'user'
expect(hasAccess(user, '/admin/users')).toBe(false);
});Everything else is irrelevant and gets a default. The test communicates intent directly.
Adding Faker for Realistic Values
Hardcoded defaults like 'user@example.com' cause collisions in parallel tests and database unique constraint violations. Use Faker.js for uniqueness:
const { faker } = require('@faker-js/faker');
function createUser(overrides = {}) {
return {
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
role: 'user',
isActive: true,
createdAt: faker.date.past(),
...overrides,
};
}Now every createUser() call produces different values — no collisions.
Building vs. Persisting
Tests that don't need a database are faster. Separate build (in-memory) from create (persisted):
const { faker } = require('@faker-js/faker');
const db = require('./db');
const UserFactory = {
build(overrides = {}) {
return {
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
role: 'user',
isActive: true,
createdAt: new Date(),
...overrides,
};
},
async create(overrides = {}) {
const data = this.build(overrides);
return db('users').insert(data).returning('*').then(rows => rows[0]);
},
async createMany(n, overrides = {}) {
return Promise.all(
Array.from({ length: n }, () => this.create(overrides))
);
},
};
module.exports = UserFactory;Usage:
// Unit test — no DB
const user = UserFactory.build({ role: 'admin' });
// Integration test — writes to DB
const user = await UserFactory.create({ role: 'admin' });
// Create 10 users
const users = await UserFactory.createMany(10);Traits
Traits are named configurations for common states:
const UserFactory = {
build(overrides = {}) {
return {
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
role: 'user',
isActive: true,
emailVerified: true,
subscriptionTier: 'free',
...overrides,
};
},
traits: {
admin: {
role: 'admin',
isStaff: true,
},
inactive: {
isActive: false,
deactivatedAt: new Date(),
},
pro: {
subscriptionTier: 'pro',
subscriptionExpiresAt: faker.date.future(),
},
unverified: {
emailVerified: false,
verificationToken: faker.string.uuid(),
},
},
with(...traitNames) {
const traitOverrides = traitNames.reduce(
(acc, name) => ({ ...acc, ...this.traits[name] }),
{}
);
return (overrides = {}) => this.build({ ...traitOverrides, ...overrides });
},
};
// Usage
const admin = UserFactory.with('admin')();
const proUser = UserFactory.with('pro')();
const inactiveAdmin = UserFactory.with('inactive', 'admin')();
const customAdmin = UserFactory.with('admin')({ name: 'Alice' });Related Objects
Factories for related objects should accept parent objects or create them automatically:
const OrderFactory = {
async create(overrides = {}) {
// Auto-create user if not provided
const user = overrides.userId
? { id: overrides.userId }
: await UserFactory.create();
const data = {
id: faker.string.uuid(),
userId: user.id,
status: 'pending',
total: faker.number.float({ min: 10, max: 500, precision: 0.01 }),
createdAt: new Date(),
...overrides,
};
const [order] = await db('orders').insert(data).returning('*');
return order;
},
};
// Creates order + user automatically
const order = await OrderFactory.create();
// Reuse an existing user
const user = await UserFactory.create();
const order1 = await OrderFactory.create({ userId: user.id });
const order2 = await OrderFactory.create({ userId: user.id });TypeScript Factories
TypeScript adds type safety to factories:
import { faker } from '@faker-js/faker';
type UserRole = 'admin' | 'editor' | 'user';
interface User {
id: string;
email: string;
name: string;
role: UserRole;
isActive: boolean;
createdAt: Date;
}
function createUser(overrides: Partial<User> = {}): User {
return {
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
role: 'user',
isActive: true,
createdAt: new Date(),
...overrides,
};
}
// TypeScript catches bad overrides at compile time
const user = createUser({ role: 'superadmin' }); // Error: not assignable to UserRoleGeneric Factory Builder
type FactoryFn<T> = (overrides?: Partial<T>) => T;
function defineFactory<T>(defaults: () => T): FactoryFn<T> {
return (overrides = {}) => ({ ...defaults(), ...overrides });
}
const createUser = defineFactory<User>(() => ({
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
role: 'user' as UserRole,
isActive: true,
createdAt: new Date(),
}));
const createProduct = defineFactory<Product>(() => ({
id: faker.string.uuid(),
name: faker.commerce.productName(),
price: parseFloat(faker.commerce.price()),
stock: faker.number.int({ min: 0, max: 1000 }),
}));Fishery: TypeScript-First Factories
For TypeScript projects with complex models, Fishery provides a more structured factory API:
npm install --save-dev fisheryimport { Factory } from 'fishery';
import { faker } from '@faker-js/faker';
import { User, UserRole } from '../src/types';
import { db } from '../src/db';
const userFactory = Factory.define<User>(({ sequence, params }) => ({
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
role: 'user' as UserRole,
isActive: true,
sequenceNumber: sequence, // built-in unique counter
createdAt: new Date(),
}));
// Usage
const user = userFactory.build();
const admin = userFactory.build({ role: 'admin' });
const users = userFactory.buildList(5);Fishery with Async (DB persistence)
const userFactory = Factory.define<User, UserTransient, UserDB>(
({ onCreate }) => {
// onCreate runs when .create() is called
onCreate(async (user) => {
const [saved] = await db('users').insert(user).returning('*');
return saved;
});
return {
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
role: 'user' as UserRole,
isActive: true,
createdAt: new Date(),
};
}
);
// DB write
const user = await userFactory.create({ role: 'admin' });
const users = await userFactory.createList(5);Fishery Traits
const userFactory = Factory.define<User>(({ transientParams }) => {
return {
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
role: 'user' as UserRole,
isActive: true,
createdAt: new Date(),
};
});
// Define trait as a separate factory
const adminFactory = userFactory.params({ role: 'admin', isStaff: true });
const inactiveFactory = userFactory.params({ isActive: false });
// Usage
const admin = adminFactory.build();
const inactiveAdmin = adminFactory.params({ isActive: false }).build();Rosie: Another JavaScript Factory Library
npm install --save-dev rosieconst { Factory } = require('rosie');
const { faker } = require('@faker-js/faker');
Factory.define('user')
.attr('id', () => faker.string.uuid())
.attr('email', () => faker.internet.email())
.attr('name', () => faker.person.fullName())
.attr('role', 'user')
.attr('isActive', true)
.option('admin', false)
.after((user, options) => {
if (options.admin) {
user.role = 'admin';
}
return user;
});
// Usage
const user = Factory.build('user');
const admin = Factory.build('user', {}, { admin: true });
const users = Factory.buildList('user', 5);Organizing Factories
Keep factories in a dedicated directory:
src/
tests/
factories/
index.js // re-exports everything
user.factory.js
order.factory.js
product.factory.js
integration/
orders.test.js
unit/
pricing.test.js// tests/factories/index.js
const { UserFactory } = require('./user.factory');
const { OrderFactory } = require('./order.factory');
const { ProductFactory } = require('./product.factory');
module.exports = { UserFactory, OrderFactory, ProductFactory };
// In tests:
const { UserFactory, OrderFactory } = require('../factories');Cleanup Strategies
Transaction Rollback (Jest)
// tests/helpers/db.js
const db = require('../../src/db');
let trx;
beforeEach(async () => {
trx = await db.transaction();
db.transacting = trx;
});
afterEach(async () => {
await trx.rollback();
});Truncate After Suite
afterAll(async () => {
await db.raw(
'TRUNCATE TABLE order_items, orders, products, users RESTART IDENTITY CASCADE'
);
await db.destroy();
});Common Pitfalls
Mutating Factory Output
// Dangerous — mutating the returned object can affect other tests
const user = UserFactory.build();
user.role = 'admin'; // Don't do this
// Right — use the factory for the correct variant
const admin = UserFactory.build({ role: 'admin' });Sharing Persisted Objects Between Tests
// Dangerous — Test B sees mutations from Test A
let user;
beforeAll(async () => {
user = await UserFactory.create();
});
test('A', async () => { user.name = 'Changed'; await user.save(); });
test('B', async () => { expect(user.name).toBe('Test User'); }); // failsFix: use beforeEach or transaction rollback.
Factories That Are Too Clever
// Overcomplicated — hard to understand and debug
function createOrderWithUser(traitNames = []) {
const base = traitNames.reduce(
(acc, t) => deepMerge(acc, traits[t]),
defaultOrder()
);
return applyPostProcessors(base, traitNames);
}
// Simple is better — explicit is better than magic
const order = createOrder({ status: 'cancelled', userId: user.id });Summary
JavaScript test data factories follow a simple progression:
- Start with plain factory functions —
createUser(overrides)gets you 80% of the way - Add Faker.js for unique, realistic values
- Separate build from create for unit vs. integration test speed
- Add traits for named states (
admin,inactive,expired) - Adopt Fishery or Rosie when you need TypeScript types, complex relationships, or team conventions
The factory pattern is the highest-leverage testing investment you can make. One well-written factory eliminates setup boilerplate from dozens of tests.