Test Data Management Strategies
A practical guide to managing test data in Node.js applications covering factories, fixtures, seeding, database cleanup, and strategies for realistic test environments.
Test Data Management Strategies
Bad test data is the root cause of most flaky tests. A test that creates a user with name: "test" and email: "[email protected]" works in isolation. Run it alongside another test that also creates email: "[email protected]" and one of them fails with a unique constraint violation. The test is not flaky — the test data strategy is broken.
Good test data management means every test creates exactly the data it needs, isolated from every other test, with realistic values that catch the same bugs your users will trigger.
Prerequisites
- Node.js installed (v16+)
- A project with database-backed tests
- Understanding of testing fundamentals
The Test Data Problems
Shared Mutable State
// BAD — tests share data and affect each other
var testUser = { id: 1, name: "Shane", email: "[email protected]" };
describe("UserService", function() {
test("updates name", function() {
testUser.name = "Updated";
var result = userService.update(testUser);
expect(result.name).toBe("Updated");
// testUser.name is now "Updated" for all subsequent tests
});
test("gets user by name", function() {
// This test might fail because the previous test changed testUser.name
var result = userService.findByName("Shane");
expect(result).toBeTruthy(); // Fails! Name is now "Updated"
});
});
Hardcoded IDs
// BAD — assumes specific IDs exist
test("gets user profile", function() {
var user = db.findById("users", 1); // Breaks if user 1 does not exist
expect(user.name).toBe("Shane"); // Breaks if user 1 has a different name
});
Incomplete Test Data
// BAD — missing required fields cause mysterious errors
test("processes order", function() {
var order = { items: [{ name: "Widget" }] };
// Crashes deep in the code because order.userId is undefined
// Error message: "Cannot read property 'email' of null"
var result = orderService.process(order);
});
Pattern 1: Factory Functions
Factory functions create test data with sensible defaults that can be overridden.
// factories.js
var nextId = 1;
function createUser(overrides) {
var id = nextId++;
return Object.assign({
id: id,
name: "User " + id,
email: "user" + id + "@example.com",
role: "user",
isActive: true,
createdAt: new Date("2026-01-01T00:00:00Z"),
updatedAt: new Date("2026-01-01T00:00:00Z")
}, overrides || {});
}
function createProduct(overrides) {
var id = nextId++;
return Object.assign({
id: id,
name: "Product " + id,
price: 9.99,
sku: "SKU-" + id,
category: "general",
inStock: true,
stockCount: 100
}, overrides || {});
}
function createOrder(overrides) {
var id = nextId++;
var user = createUser();
return Object.assign({
id: id,
userId: user.id,
user: user,
items: [createOrderItem()],
status: "pending",
subtotal: 9.99,
tax: 0.80,
total: 10.79,
createdAt: new Date("2026-01-15T10:00:00Z")
}, overrides || {});
}
function createOrderItem(overrides) {
var product = createProduct();
return Object.assign({
productId: product.id,
product: product,
quantity: 1,
unitPrice: product.price,
lineTotal: product.price
}, overrides || {});
}
function resetIds() {
nextId = 1;
}
module.exports = {
createUser: createUser,
createProduct: createProduct,
createOrder: createOrder,
createOrderItem: createOrderItem,
resetIds: resetIds
};
Using Factories in Tests
var factory = require("./factories");
describe("OrderService", function() {
beforeEach(function() {
factory.resetIds();
});
test("calculates total with multiple items", function() {
var order = factory.createOrder({
items: [
factory.createOrderItem({ quantity: 2, unitPrice: 10 }),
factory.createOrderItem({ quantity: 1, unitPrice: 25 })
]
});
var result = orderService.calculateTotal(order);
expect(result).toBe(45);
});
test("applies discount for premium users", function() {
var user = factory.createUser({ role: "premium" });
var order = factory.createOrder({ userId: user.id, user: user });
var result = orderService.applyDiscount(order);
expect(result.discount).toBeGreaterThan(0);
});
test("rejects orders from inactive users", function() {
var user = factory.createUser({ isActive: false });
var order = factory.createOrder({ userId: user.id, user: user });
expect(function() {
orderService.validate(order);
}).toThrow("User account is inactive");
});
});
Pattern 2: Builder Pattern
For complex objects with many optional fields, builders provide a fluent API:
// builders.js
function UserBuilder() {
this._data = {
name: "Default User",
email: "[email protected]",
role: "user",
isActive: true,
preferences: {}
};
}
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";
return this;
};
UserBuilder.prototype.inactive = function() {
this._data.isActive = false;
return this;
};
UserBuilder.prototype.withPreferences = function(prefs) {
this._data.preferences = prefs;
return this;
};
UserBuilder.prototype.build = function() {
return Object.assign({}, this._data);
};
function aUser() {
return new UserBuilder();
}
module.exports = { aUser: aUser };
var { aUser } = require("./builders");
test("admin users can delete posts", function() {
var admin = aUser().withName("Admin Shane").asAdmin().build();
var result = postService.delete(admin, postId);
expect(result.deleted).toBe(true);
});
test("inactive users cannot create posts", function() {
var inactive = aUser().inactive().build();
expect(function() {
postService.create(inactive, postData);
}).toThrow("Account inactive");
});
Pattern 3: Database Fixtures
Fixtures are predefined data sets loaded into the database before tests.
// fixtures/users.js
var users = [
{
id: 1,
name: "Alice Admin",
email: "[email protected]",
role: "admin",
isActive: true
},
{
id: 2,
name: "Bob Regular",
email: "[email protected]",
role: "user",
isActive: true
},
{
id: 3,
name: "Carol Inactive",
email: "[email protected]",
role: "user",
isActive: false
}
];
module.exports = users;
// fixtures/products.js
var products = [
{ id: 1, name: "Widget", price: 9.99, sku: "WDG-001", inStock: true },
{ id: 2, name: "Gadget", price: 24.99, sku: "GDG-001", inStock: true },
{ id: 3, name: "Doohickey", price: 4.99, sku: "DHK-001", inStock: false }
];
module.exports = products;
Fixture Loader
// test/helpers/fixtures.js
var db = require("../../src/db");
function loadFixtures(fixtures) {
var tables = Object.keys(fixtures);
var promises = [];
for (var i = 0; i < tables.length; i++) {
var table = tables[i];
var records = fixtures[table];
for (var j = 0; j < records.length; j++) {
promises.push(db.insert(table, records[j]));
}
}
return Promise.all(promises);
}
function clearTables(tableNames) {
var promises = [];
for (var i = 0; i < tableNames.length; i++) {
promises.push(db.clear(tableNames[i]));
}
return Promise.all(promises);
}
module.exports = {
loadFixtures: loadFixtures,
clearTables: clearTables
};
var fixtures = require("./helpers/fixtures");
var users = require("./fixtures/users");
var products = require("./fixtures/products");
describe("OrderService", function() {
beforeEach(function() {
return fixtures.clearTables(["orders", "users", "products"])
.then(function() {
return fixtures.loadFixtures({
users: users,
products: products
});
});
});
test("creates order for active user", function() {
var order = orderService.create(1, [{ productId: 1, quantity: 2 }]);
expect(order.status).toBe("pending");
expect(order.total).toBe(19.98);
});
});
Pattern 4: Database Cleanup Strategies
Truncation (Fast, Parallel-Safe)
// Clean tables between tests
beforeEach(function() {
return db.query("TRUNCATE users, orders, products RESTART IDENTITY CASCADE");
});
Transaction Rollback (Fastest)
Wrap each test in a transaction and roll it back:
var db = require("./db");
describe("UserService", function() {
var transaction;
beforeEach(function() {
return db.beginTransaction().then(function(tx) {
transaction = tx;
// All database operations use this transaction
db.setTransaction(transaction);
});
});
afterEach(function() {
return transaction.rollback();
});
test("creates a user", function() {
// This insert is rolled back after the test
return db.insert("users", { name: "Shane", email: "[email protected]" })
.then(function(user) {
expect(user.id).toBeTruthy();
});
});
});
Delete in Reverse Order
When foreign keys prevent truncation:
function cleanDatabase() {
// Delete in reverse dependency order
return db.query("DELETE FROM order_items")
.then(function() { return db.query("DELETE FROM orders"); })
.then(function() { return db.query("DELETE FROM products"); })
.then(function() { return db.query("DELETE FROM users"); });
}
Pattern 5: Unique Data Generation
Avoid collisions between parallel tests:
// unique.js
var crypto = require("crypto");
function uniqueId() {
return crypto.randomBytes(4).toString("hex");
}
function uniqueEmail() {
return "test-" + uniqueId() + "@example.com";
}
function uniqueUsername() {
return "user-" + uniqueId();
}
function createUniqueUser(overrides) {
var id = uniqueId();
return Object.assign({
name: "User " + id,
email: "user-" + id + "@example.com",
role: "user",
isActive: true
}, overrides || {});
}
module.exports = {
uniqueId: uniqueId,
uniqueEmail: uniqueEmail,
uniqueUsername: uniqueUsername,
createUniqueUser: createUniqueUser
};
var unique = require("./unique");
test("registers a new user", function() {
var email = unique.uniqueEmail();
return userService.register("Test User", email, "password123")
.then(function(user) {
expect(user.email).toBe(email);
});
});
// This test can run in parallel with the one above — no collision
test("rejects duplicate email", function() {
var email = unique.uniqueEmail();
return userService.register("First", email, "pass1")
.then(function() {
return expect(
userService.register("Second", email, "pass2")
).rejects.toThrow("Email already registered");
});
});
Pattern 6: Seed Scripts for Development and Staging
// seeds/development.js
var db = require("../src/db");
var crypto = require("crypto");
function hashPassword(password) {
return crypto.createHash("sha256").update(password).digest("hex");
}
function seed() {
console.log("Seeding development database...");
return db.clear("users")
.then(function() { return db.clear("products"); })
.then(function() { return db.clear("orders"); })
.then(function() {
// Create users
var users = [];
for (var i = 1; i <= 50; i++) {
users.push({
name: "Dev User " + i,
email: "user" + i + "@dev.example.com",
password: hashPassword("password" + i),
role: i <= 5 ? "admin" : "user",
isActive: i <= 45 // 5 inactive users
});
}
return Promise.all(users.map(function(u) { return db.insert("users", u); }));
})
.then(function() {
// Create products
var categories = ["electronics", "books", "clothing", "food", "tools"];
var products = [];
for (var i = 1; i <= 100; i++) {
products.push({
name: "Product " + i,
price: Math.round((Math.random() * 99 + 1) * 100) / 100,
sku: "SKU-" + String(i).padStart(5, "0"),
category: categories[i % categories.length],
inStock: Math.random() > 0.1,
stockCount: Math.floor(Math.random() * 500)
});
}
return Promise.all(products.map(function(p) { return db.insert("products", p); }));
})
.then(function() {
console.log("Seeding complete: 50 users, 100 products");
});
}
if (require.main === module) {
seed()
.then(function() { process.exit(0); })
.catch(function(err) {
console.error("Seeding failed:", err);
process.exit(1);
});
}
module.exports = seed;
{
"scripts": {
"seed": "node seeds/development.js",
"seed:test": "NODE_ENV=test node seeds/test.js"
}
}
Pattern 7: Snapshot Data for Complex Objects
When test data is complex, store it as JSON snapshots:
// test/data/complex-order.json
{
"userId": 1,
"items": [
{
"productId": 101,
"quantity": 2,
"unitPrice": 29.99,
"options": {
"color": "blue",
"size": "large",
"giftWrap": true
}
},
{
"productId": 205,
"quantity": 1,
"unitPrice": 149.99,
"options": {
"warranty": "extended",
"shipping": "express"
}
}
],
"shippingAddress": {
"street": "123 Main St",
"city": "Oakland",
"state": "CA",
"zip": "94612"
},
"couponCode": "SAVE10"
}
var orderData = require("./data/complex-order.json");
test("processes complex order", function() {
// Use the snapshot data but override specific fields
var testOrder = Object.assign({}, orderData, {
userId: testUser.id
});
var result = orderService.process(testOrder);
expect(result.status).toBe("confirmed");
});
Pattern 8: Time-Dependent Test Data
// time-helpers.js
function daysAgo(n) {
var date = new Date();
date.setDate(date.getDate() - n);
return date;
}
function daysFromNow(n) {
var date = new Date();
date.setDate(date.getDate() + n);
return date;
}
function hoursAgo(n) {
var date = new Date();
date.setHours(date.getHours() - n);
return date;
}
function createExpiredSubscription(overrides) {
return Object.assign({
userId: 1,
plan: "premium",
startDate: daysAgo(365),
endDate: daysAgo(1),
status: "expired"
}, overrides || {});
}
function createActiveSubscription(overrides) {
return Object.assign({
userId: 1,
plan: "premium",
startDate: daysAgo(30),
endDate: daysFromNow(335),
status: "active"
}, overrides || {});
}
module.exports = {
daysAgo: daysAgo,
daysFromNow: daysFromNow,
hoursAgo: hoursAgo,
createExpiredSubscription: createExpiredSubscription,
createActiveSubscription: createActiveSubscription
};
var helpers = require("./time-helpers");
test("denies access for expired subscriptions", function() {
var sub = helpers.createExpiredSubscription({ userId: testUser.id });
db.insert("subscriptions", sub);
var result = accessService.checkAccess(testUser.id, "premium-content");
expect(result.allowed).toBe(false);
expect(result.reason).toBe("Subscription expired");
});
test("grants access for active subscriptions", function() {
var sub = helpers.createActiveSubscription({ userId: testUser.id });
db.insert("subscriptions", sub);
var result = accessService.checkAccess(testUser.id, "premium-content");
expect(result.allowed).toBe(true);
});
Complete Test Setup Example
Putting all patterns together:
// test/setup.js
var db = require("../src/db");
var factory = require("./factories");
var testContext = {};
function setupTestDatabase() {
return db.connect(process.env.TEST_DATABASE_URL);
}
function cleanDatabase() {
return db.query("TRUNCATE users, products, orders, order_items RESTART IDENTITY CASCADE");
}
function createStandardTestData() {
return cleanDatabase()
.then(function() {
return db.insert("users", factory.createUser({ name: "Test Admin", role: "admin" }));
})
.then(function(admin) {
testContext.admin = admin;
return db.insert("users", factory.createUser({ name: "Test User" }));
})
.then(function(user) {
testContext.user = user;
return db.insert("products", factory.createProduct({ name: "Widget", price: 10 }));
})
.then(function(product) {
testContext.product = product;
return testContext;
});
}
module.exports = {
setupTestDatabase: setupTestDatabase,
cleanDatabase: cleanDatabase,
createStandardTestData: createStandardTestData,
testContext: testContext
};
// test/orderService.test.js
var setup = require("./setup");
var factory = require("./factories");
var orderService = require("../src/orderService");
describe("OrderService", function() {
beforeAll(function() {
return setup.setupTestDatabase();
});
beforeEach(function() {
return setup.createStandardTestData();
});
test("creates order for active user", function() {
var ctx = setup.testContext;
return orderService.create(ctx.user.id, [
{ productId: ctx.product.id, quantity: 2 }
]).then(function(order) {
expect(order.userId).toBe(ctx.user.id);
expect(order.total).toBe(20);
expect(order.status).toBe("pending");
});
});
test("admin can view all orders", function() {
var ctx = setup.testContext;
// Create an order first
return orderService.create(ctx.user.id, [
{ productId: ctx.product.id, quantity: 1 }
]).then(function() {
return orderService.listAll(ctx.admin.id);
}).then(function(orders) {
expect(orders.length).toBe(1);
});
});
});
Common Issues and Troubleshooting
Tests pass individually but fail when run together
Shared state between tests — one test modifies data that another test depends on:
Fix: Use beforeEach to reset state, not beforeAll. Give each test its own data with factory functions. Use unique values (unique emails, random IDs) to avoid collisions.
Database cleanup is slow
Truncating many tables takes time, especially with foreign keys:
Fix: Use transaction rollback instead of truncation — it is instant. If you must truncate, use CASCADE to avoid foreign key issues. Order deletions to respect foreign key constraints.
Test data does not match production schema
Factory functions create objects missing fields that production code expects:
Fix: Keep factory functions updated when the schema changes. Add a CI check that compares factory output against the actual schema. Use database migrations in test setup to ensure the test database matches production.
Parallel tests conflict on unique constraints
Two parallel tests create users with the same email:
Fix: Use uniqueEmail() and uniqueId() helpers instead of hardcoded values. Run tests that share a database in sequence, or use per-test transactions for isolation.
Best Practices
- Use factory functions for all test data. Never hardcode test data inline. Factories provide defaults, prevent duplication, and make tests readable.
- Each test creates its own data. Do not rely on data from other tests or from a shared setup. Tests must be independent and runnable in any order.
- Reset state in beforeEach, not beforeAll.
beforeAllruns once for the entire suite. If one test modifies shared state, subsequent tests see the modification. - Use unique values to avoid collisions. Random emails, usernames, and IDs prevent tests from conflicting when run in parallel.
- Clean up after yourself. Whether through transaction rollback, truncation, or explicit deletion, ensure each test starts with a clean slate.
- Keep test data realistic. Fake data should resemble production data in structure, types, and edge cases. An email of "x" will not catch validation bugs that "user@example" will.
- Separate test data helpers from test logic. Put factories, builders, and fixtures in dedicated files. Tests should focus on behavior, not data construction.
- Document your test data patterns. New team members need to understand how to create test data. A
test/README.mdexplaining the factories and setup helpers saves onboarding time.
References
- Factory Bot (Ruby) — inspiration for factory patterns
- Fishery (TypeScript Factories)
- Test Data Builders
- Jest Setup and Teardown
- Faker.js — realistic fake data generation