Test Data Factories in JavaScript: Patterns, Libraries, and TypeScript
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.