Test Fixtures and Factories
A practical guide to creating reusable test data with fixtures, factory functions, and builder patterns in Node.js testing with Jest and Mocha.
Test Fixtures and Factories
Every test needs data. The question is where that data comes from and how it is maintained. Inline test data clutters tests with irrelevant details. Shared global data creates hidden dependencies between tests. Hardcoded IDs break when the database resets.
Fixtures and factories solve these problems with different tradeoffs. Fixtures are static data files loaded into the database — predictable, version-controlled, and shared across tests. Factories are functions that generate data on demand — flexible, unique, and self-contained. Most projects benefit from using both.
Prerequisites
- Node.js installed (v16+)
- A project with Jest or Mocha tests
- Understanding of testing fundamentals
Fixtures: Static Test Data
Fixtures are predefined data stored in files and loaded into the test database before tests run.
JSON Fixtures
// test/fixtures/users.json
[
{
"name": "Alice Admin",
"email": "[email protected]",
"role": "admin",
"isActive": true,
"createdAt": "2026-01-01T00:00:00Z"
},
{
"name": "Bob User",
"email": "[email protected]",
"role": "user",
"isActive": true,
"createdAt": "2026-01-05T00:00:00Z"
},
{
"name": "Carol Inactive",
"email": "[email protected]",
"role": "user",
"isActive": false,
"createdAt": "2025-06-15T00:00:00Z"
}
]
// test/fixtures/products.json
[
{
"name": "Standard Widget",
"price": 29.99,
"sku": "WDG-001",
"category": "widgets",
"inStock": true,
"stockCount": 150
},
{
"name": "Premium Gadget",
"price": 99.99,
"sku": "GDG-001",
"category": "gadgets",
"inStock": true,
"stockCount": 42
},
{
"name": "Discontinued Doohickey",
"price": 4.99,
"sku": "DHK-001",
"category": "misc",
"inStock": false,
"stockCount": 0
}
]
Fixture Loader
// test/helpers/fixtureLoader.js
var fs = require("fs");
var path = require("path");
var db = require("../../src/db");
var fixturesDir = path.join(__dirname, "..", "fixtures");
function loadFixture(tableName, filename) {
var filePath = path.join(fixturesDir, filename || tableName + ".json");
var data = JSON.parse(fs.readFileSync(filePath, "utf8"));
var promises = data.map(function(record) {
return db.insert(tableName, record);
});
return Promise.all(promises);
}
function loadAll(fixtures) {
var tables = Object.keys(fixtures);
var chain = Promise.resolve();
// Load in order to respect foreign keys
tables.forEach(function(table) {
chain = chain.then(function() {
return loadFixture(table, fixtures[table]);
});
});
return chain;
}
function clearAll(tableNames) {
return db.query(
"TRUNCATE " + tableNames.join(", ") + " RESTART IDENTITY CASCADE"
);
}
module.exports = {
loadFixture: loadFixture,
loadAll: loadAll,
clearAll: clearAll
};
Using Fixtures in Tests
var fixtureLoader = require("./helpers/fixtureLoader");
describe("Product Catalog", function() {
beforeEach(function() {
return fixtureLoader.clearAll(["orders", "products", "users"])
.then(function() {
return fixtureLoader.loadAll({
users: "users.json",
products: "products.json"
});
});
});
test("lists in-stock products", function() {
return productService.listAvailable().then(function(products) {
expect(products).toHaveLength(2);
products.forEach(function(p) {
expect(p.inStock).toBe(true);
});
});
});
test("finds product by SKU", function() {
return productService.findBySku("WDG-001").then(function(product) {
expect(product.name).toBe("Standard Widget");
expect(product.price).toBe(29.99);
});
});
});
When Fixtures Work Best
- Stable reference data — categories, roles, configuration that rarely changes
- Read-only tests — tests that query but do not modify data
- Reproducible scenarios — specific data combinations for edge case testing
- Snapshot testing — comparing query results against known data
When Fixtures Cause Problems
- Tests that modify data — one test changes a fixture record, breaking other tests
- Unique constraints — fixed emails or usernames conflict with dynamically created records
- Growing maintenance — adding a new column requires updating every fixture file
- Hidden coupling — tests depend on specific fixture IDs without making the dependency obvious
Factories: Dynamic Test Data
Factory functions generate test data on demand with unique values and sensible defaults.
Basic Factory
// test/factories/userFactory.js
var crypto = require("crypto");
var sequence = 0;
function nextId() {
sequence++;
return sequence;
}
function uniqueString() {
return crypto.randomBytes(4).toString("hex");
}
function createUser(overrides) {
var id = nextId();
var unique = uniqueString();
return Object.assign({
name: "User " + id,
email: "user-" + unique + "@example.com",
role: "user",
isActive: true,
password: "hashed-password-" + unique,
createdAt: new Date("2026-01-01T00:00:00Z"),
updatedAt: new Date("2026-01-01T00:00:00Z")
}, overrides || {});
}
function createAdmin(overrides) {
return createUser(Object.assign({ role: "admin" }, overrides || {}));
}
function createInactiveUser(overrides) {
return createUser(Object.assign({ isActive: false }, overrides || {}));
}
function resetSequence() {
sequence = 0;
}
module.exports = {
createUser: createUser,
createAdmin: createAdmin,
createInactiveUser: createInactiveUser,
resetSequence: resetSequence
};
Related Object Factories
// test/factories/orderFactory.js
var userFactory = require("./userFactory");
var productFactory = require("./productFactory");
function createOrder(overrides) {
var user = (overrides && overrides.user) || userFactory.createUser();
var items = (overrides && overrides.items) || [createOrderItem()];
var subtotal = items.reduce(function(sum, item) {
return sum + item.lineTotal;
}, 0);
return Object.assign({
userId: user.id || null,
user: user,
items: items,
subtotal: subtotal,
tax: Math.round(subtotal * 0.08 * 100) / 100,
total: Math.round(subtotal * 1.08 * 100) / 100,
status: "pending",
createdAt: new Date()
}, overrides || {});
}
function createOrderItem(overrides) {
var product = (overrides && overrides.product) || productFactory.createProduct();
var quantity = (overrides && overrides.quantity) || 1;
return Object.assign({
productId: product.id || null,
product: product,
quantity: quantity,
unitPrice: product.price,
lineTotal: product.price * quantity
}, overrides || {});
}
function createCompletedOrder(overrides) {
return createOrder(Object.assign({
status: "completed",
completedAt: new Date()
}, overrides || {}));
}
module.exports = {
createOrder: createOrder,
createOrderItem: createOrderItem,
createCompletedOrder: createCompletedOrder
};
Database-Backed Factories
Factories that insert records into the database and return the created records with real IDs:
// test/factories/dbFactory.js
var db = require("../../src/db");
var userFactory = require("./userFactory");
var productFactory = require("./productFactory");
function insertUser(overrides) {
var data = userFactory.createUser(overrides);
delete data.id; // Let the database assign the ID
return db.insert("users", data);
}
function insertProduct(overrides) {
var data = productFactory.createProduct(overrides);
delete data.id;
return db.insert("products", data);
}
function insertOrder(userId, items) {
var subtotal = items.reduce(function(sum, item) {
return sum + (item.unitPrice * item.quantity);
}, 0);
return db.insert("orders", {
userId: userId,
subtotal: subtotal,
tax: Math.round(subtotal * 0.08 * 100) / 100,
total: Math.round(subtotal * 1.08 * 100) / 100,
status: "pending"
}).then(function(order) {
var itemPromises = items.map(function(item) {
return db.insert("order_items", {
orderId: order.id,
productId: item.productId,
quantity: item.quantity,
unitPrice: item.unitPrice
});
});
return Promise.all(itemPromises).then(function(orderItems) {
order.items = orderItems;
return order;
});
});
}
module.exports = {
insertUser: insertUser,
insertProduct: insertProduct,
insertOrder: insertOrder
};
Using Database Factories in Tests
var dbFactory = require("./factories/dbFactory");
describe("Order Processing", function() {
beforeEach(function() {
return db.query("TRUNCATE orders, order_items, products, users RESTART IDENTITY CASCADE");
});
test("calculates order total correctly", function() {
var user, product1, product2;
return dbFactory.insertUser({ name: "Buyer" })
.then(function(u) {
user = u;
return Promise.all([
dbFactory.insertProduct({ name: "Widget", price: 10 }),
dbFactory.insertProduct({ name: "Gadget", price: 25 })
]);
})
.then(function(products) {
product1 = products[0];
product2 = products[1];
return dbFactory.insertOrder(user.id, [
{ productId: product1.id, quantity: 3, unitPrice: product1.price },
{ productId: product2.id, quantity: 1, unitPrice: product2.price }
]);
})
.then(function(order) {
expect(order.subtotal).toBe(55);
expect(order.total).toBe(59.40); // 55 * 1.08
});
});
});
Builder Pattern
For objects with many fields and complex construction logic:
// test/builders/userBuilder.js
function UserBuilder() {
this._data = {
name: "Default User",
email: "default-" + Date.now() + "@example.com",
role: "user",
isActive: true,
preferences: { theme: "light", notifications: true },
address: null
};
}
UserBuilder.prototype.withName = function(name) {
this._data.name = name;
return this;
};
UserBuilder.prototype.withEmail = function(email) {
this._data.email = email;
return this;
};
UserBuilder.prototype.asAdmin = function() {
this._data.role = "admin";
return this;
};
UserBuilder.prototype.asPremium = function() {
this._data.role = "premium";
this._data.preferences.theme = "dark";
return this;
};
UserBuilder.prototype.inactive = function() {
this._data.isActive = false;
return this;
};
UserBuilder.prototype.withAddress = function(street, city, state, zip) {
this._data.address = {
street: street,
city: city,
state: state,
zip: zip
};
return this;
};
UserBuilder.prototype.withDarkTheme = function() {
this._data.preferences.theme = "dark";
return this;
};
UserBuilder.prototype.withNotificationsOff = function() {
this._data.preferences.notifications = false;
return this;
};
UserBuilder.prototype.build = function() {
return JSON.parse(JSON.stringify(this._data));
};
UserBuilder.prototype.insert = function(db) {
var data = this.build();
return db.insert("users", data);
};
function aUser() {
return new UserBuilder();
}
module.exports = { aUser: aUser, UserBuilder: UserBuilder };
var { aUser } = require("./builders/userBuilder");
test("premium users see dark theme by default", function() {
var user = aUser()
.withName("Premium Pete")
.asPremium()
.build();
expect(user.preferences.theme).toBe("dark");
});
test("inactive users cannot place orders", function() {
var user = aUser()
.inactive()
.build();
expect(function() {
orderService.validate({ user: user, items: [{ id: 1 }] });
}).toThrow("Account inactive");
});
test("admin user with specific address", function() {
var user = aUser()
.asAdmin()
.withName("Admin Alice")
.withAddress("123 Main St", "Oakland", "CA", "94612")
.build();
expect(user.role).toBe("admin");
expect(user.address.city).toBe("Oakland");
});
Factory Index: Central Registry
// test/factories/index.js
var userFactory = require("./userFactory");
var productFactory = require("./productFactory");
var orderFactory = require("./orderFactory");
var dbFactory = require("./dbFactory");
module.exports = {
// In-memory factories
user: userFactory.createUser,
admin: userFactory.createAdmin,
product: productFactory.createProduct,
order: orderFactory.createOrder,
orderItem: orderFactory.createOrderItem,
// Database-backed factories
db: {
user: dbFactory.insertUser,
product: dbFactory.insertProduct,
order: dbFactory.insertOrder
},
// Reset sequences
reset: function() {
userFactory.resetSequence();
productFactory.resetSequence();
}
};
var factory = require("./factories");
test("creates a complete order", function() {
var user = factory.user({ name: "Buyer" });
var product = factory.product({ price: 25 });
var order = factory.order({
user: user,
items: [factory.orderItem({ product: product, quantity: 3 })]
});
expect(order.total).toBe(81); // 75 * 1.08
});
Choosing Between Fixtures and Factories
| Scenario | Use Fixtures | Use Factories |
|---|---|---|
| Reference data (categories, roles) | Yes | No |
| Data that tests modify | No | Yes |
| Complex object graphs | Sometimes | Yes |
| Unique constraint fields (email) | Carefully | Yes |
| Large datasets for queries | Yes | Sometimes |
| Cross-test data sharing | Yes | No |
| Tests that create records | No | Yes |
| Snapshot-style testing | Yes | No |
Best practice: Use fixtures for stable reference data that tests read but do not modify. Use factories for test-specific data that each test creates and owns.
Common Issues and Troubleshooting
Fixture data conflicts with factory-generated data
A fixture creates email: "[email protected]" and a factory also generates that email:
Fix: Use clearly distinct domains: fixtures use @fixture.test, factories use @factory.test. Or use unique suffixes in factory emails. Keep fixture emails deterministic and factory emails random.
Factory-created objects are missing required fields
A field was added to the database but the factory was not updated:
Fix: Keep factories close to the schema. When you add a migration, update the corresponding factory. Add a test that validates factory output against the schema.
Test data is too complex to understand
Tests create deeply nested objects with dozens of fields:
Fix: Use the builder pattern with descriptive method names. aUser().asAdmin().inactive() is clearer than createUser({ role: "admin", isActive: false }). Only override fields relevant to the test.
Sequence IDs cause test order dependency
Factory sequence numbers change depending on test execution order:
Fix: Never assert on factory-generated IDs. Use resetSequence() in beforeEach if deterministic IDs matter. Prefer asserting on business fields (name, email) rather than technical fields (id).
Best Practices
- Every test creates its own data. Tests must not depend on data created by other tests. Use
beforeEachto reset state and create fresh data. - Factory defaults should be valid. Calling
factory.user()with no overrides should produce a record that passes all validation. The test only needs to override what it is testing. - Keep factories next to their tests. Place factory files in the test directory, not in the source directory. They are testing infrastructure, not application code.
- Use unique values for unique constraints. Never hardcode emails, usernames, or other unique fields in factories. Generate unique values to prevent collisions in parallel tests.
- Override only what matters for the test. If testing admin permissions, override
role: "admin". Do not also overridename,email, andcreatedAtunless they affect the test outcome. - Version control your fixtures. Fixture files are part of your test suite. Review changes to fixtures like code changes — they can introduce or hide bugs.
- Clean up between tests. Truncate tables or roll back transactions in
beforeEach. Leaked data between tests is the most common source of test flakiness. - Combine fixtures and factories. Use fixtures for shared reference data, factories for test-specific records. This gives you the stability of fixtures with the flexibility of factories.
References
- Factory Bot (Ruby) — the pattern origin
- Fishery (TypeScript Factories)
- Jest Setup and Teardown
- Test Data Builders
- Faker.js