Test Data Generation
Overview
Test data generation creates realistic, consistent, and maintainable test data for automated testing. Well-designed test data reduces test brittleness, improves readability, and makes it easier to create diverse test scenarios.
When to Use
- Creating fixtures for integration tests
- Generating fake data for development databases
- Building test data with complex relationships
- Creating realistic user inputs for testing
- Seeding test databases
- Generating edge cases and boundary values
- Building reusable test data factories
Instructions
1. Factory Pattern for Test Data
JavaScript/Jest with Factory Functions
// tests/factories/userFactory.js
const { faker } = require('@faker-js/faker');
class UserFactory {
static build(overrides = {}) {
return {
id: faker.string.uuid(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
age: faker.number.int({ min: 18, max: 80 }),
phone: faker.phone.number(),
address: {
street: faker.location.streetAddress(),
city: faker.location.city(),
state: faker.location.state(),
zip: faker.location.zipCode(),
country: 'USA'
},
role: 'user',
isActive: true,
createdAt: faker.date.past(),
...overrides
};
}
static buildMany(count, overrides = {}) {
return Array.from({ length: count }, () => this.build(overrides));
}
static buildAdmin(overrides = {}) {
return this.build({
role: 'admin',
permissions: ['read', 'write', 'delete'],
...overrides
});
}
static buildInactive(overrides = {}) {
return this.build({
isActive: false,
deactivatedAt: faker.date.recent(),
...overrides
});
}
}
// tests/user.test.js
describe('User Service', () => {
test('should create user with valid data', () => {
const userData = UserFactory.build();
const user = userService.create(userData);
expect(user.email).toBe(userData.email);
expect(user.isActive).toBe(true);
});
test('should handle admin users differently', () => {
const admin = UserFactory.buildAdmin();
expect(admin.role).toBe('admin');
expect(admin.permissions).toContain('delete');
});
test('should process multiple users', () => {
const users = UserFactory.buildMany(5);
expect(users).toHaveLength(5);
expect(new Set(users.map(u => u.email)).size).toBe(5); // All unique
});
});
Python with Factory Boy
# tests/factories.py
import factory
from factory.faker import Faker
from datetime import datetime, timedelta
from app.models import User, Order, Product
class UserFactory(factory.Factory):
class Meta:
model = User
id = factory.Sequence(lambda n: n)
email = Faker('email')
first_name = Faker('first_name')
last_name = Faker('last_name')
username = factory.LazyAttribute(
lambda obj: f"{obj.first_name.lower()}.{obj.last_name.lower()}"
)
age = Faker('random_int', min=18, max=80)
phone = Faker('phone_number')
is_active = True
role = 'user'
created_at = Faker('date_time_this_year')
class Params:
# Traits for different user types
admin = factory.Trait(
role='admin',
permissions=['read', 'write', 'delete']
)
inactive = factory.Trait(
is_active=False,
deactivated_at=factory.LazyFunction(datetime.now)
)
premium = factory.Trait(
subscription='premium',
subscription_end=factory.LazyFunction(
lambda: datetime.now() + timedelta(days=365)
)
)
class ProductFactory(factory.Factory):
class Meta:
model = Product
id = factory.Sequence(lambda n: n)
name = Faker('commerce_product_name')
description = Faker('text', max_nb_chars=200)
price = Faker('pydecimal', left_digits=3, right_digits=2, positive=True)
sku = factory.LazyAttribute(
lambda obj: f"SKU-{obj.id:06d}"
)
stock = Faker('random_int', min=0, max=100)
category = Faker('random_element', elements=['electronics', 'clothing', 'books'])
is_available = factory.LazyAttribute(lambda obj: obj.stock > 0)
class OrderFactory(factory.Factory):
class Meta:
model = Order
id = factory.Sequence(lambda n: n)
user = factory.SubFactory(UserFactory)
status = 'pending'
total = Faker('pydecimal', left_digits=4, right_digits=2, positive=True)
created_at = Faker('date_time_this_month')
@factory.post_generation
def products(self, create, extracted, **kwargs):
"""Add products to order after creation."""
if not create:
return
if extracted:
for product in extracted:
self.products.add(product)
else:
# Add 1-3 random products by default
count = kwargs.get('count', 3)
self.products.add(*ProductFactory.build_batch(count))
# tests/test_orders.py
import pytest
from tests.factories import UserFactory, OrderFactory, ProductFactory
def test_create_order_with_products():
"""Test order creation with specific products."""
products = ProductFactory.build_batch(3)
order = OrderFactory.build(products=products)
assert order.user is not None
assert len(order.products) == 3
assert order.status == 'pending'
def test_admin_user_permissions():
"""Test admin user has correct permissions."""
admin = UserFactory.build(admin=True)
assert admin.role == 'admin'
assert 'delete' in admin.permissions
def test_inactive_user():
"""Test inactive user properties."""
user = UserFactory.build(inactive=True)
assert not user.is_active
assert user.deactivated_at is not None
def test_bulk_user_creation():
"""Test creating multiple users at once."""
users = UserFactory.build_batch(10, role='user')
assert len(users) == 10
assert all(u.role == 'user' for u in users)
# All emails should be unique
assert len(set(u.email for u in users)) == 10
2. Builder Pattern for Complex Objects
// tests/builders/OrderBuilder.ts
import { faker } from '@faker-js/faker';
export class OrderBuilder {
private order: Partial<Order> = {
id: faker.string.uuid(),
status: 'pending',
items: [],
total: 0,
createdAt: new Date(),
};
withId(id: string): this {
this.order.id = id;
return this;
}
withStatus(status: OrderStatus): this {
this.order.status = status;
return this;
}
withUser(user: User): this {
this.order.userId = user.id;
this.order.user = user;
return this;
}
withItems(items: OrderItem[]): this {
this.order.items = items;
this.order.total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
return this;
}
addItem(product: Product, quantity: number = 1): this {
const item: OrderItem = {
productId: product.id,
product,
quantity,
price: product.price,
subtotal: product.price * quantity,
};
this.order.items = [...(this.order.items || []), item];
this.order.total = (this.order.total || 0) + item.subtotal;
return this;
}
withShippingAddress(address: Address): this {
this.order.shippingAddress = address;
return this;
}
asPaid(): this {
this.order.status = 'paid';
this.order.paidAt = new Date();
return this;
}
asShipped(): this {
this.order.status = 'shipped';
this.order.shippedAt = new Date();
return this;
}
build(): Order {
return this.order as Order;
}
}
// Usage in tests
describe('Order Processing', () => {
it('should calculate total correctly', () => {
const product1 = ProductBuilder.aProduct().withPrice(10.00).build();
const product2 = ProductBuilder.aProduct().withPrice(25.00).build();
const order = new OrderBuilder()
.withUser(UserBuilder.aUser().build())
.addItem(product1, 2) // $20
.addItem(product2, 1) // $25
.build();
expect(order.total).toBe(45.00);
expect(order.items).toHaveLength(2);
});
it('should process paid orders', () => {
const order = new OrderBuilder()
.withUser(UserBuilder.aUser().build())
.addItem(ProductBuilder.aProduct().build())
.asPaid()
.build();
expect(order.status).toBe('paid');
expect(order.paidAt).toBeDefined();
});
});
3. Fixtures for Integration Tests
Jest/TypeScript with Database Fixtures
// tests/fixtures/database.ts
import { PrismaClient } from '@prisma/client';
import { UserFactory, ProductFactory, OrderFactory } from './factories';
export class DatabaseFixtures {
constructor(private prisma: PrismaClient) {}
async seed() {
// Create users
const users = await Promise.all(
UserFactory.buildMany(10).map(userData =>
this.prisma.user.create({ data: userData })
)
);
// Create products
const products = await Promise.all(
ProductFactory.buildMany(20).map(productData =>
this.prisma.product.create({ data: productData })
)
);
// Create orders
const orders = await Promise.all(
OrderFactory.buildMany(15).map(orderData =>
this.prisma.order.create({
data: {
...orderData,
userId: users[Math.floor(Math.random() * users.length)].id,
items: {
create: products.slice(0, 3).map(product => ({
productId: product.id,
quantity: Math.floor(Math.random() * 3) + 1,
price: product.price,
})),
},
},
})
)
);
return { users, products, orders };
}
async clear() {
await this.prisma.orderItem.deleteMany();
await this.prisma.order.deleteMany();
await this.prisma.product.deleteMany();
await this.prisma.user.deleteMany();
}
}
// tests/setup.ts
import { PrismaClient } from '@prisma/client';
import { DatabaseFixtures } from './fixtures/database';
const prisma = new PrismaClient();
const fixtures = new DatabaseFixtures(prisma);
beforeAll(async () => {
await fixtures.clear();
await fixtures.seed();
});
afterAll(async () => {
await fixtures.clear();
await prisma.$disconnect();
});
pytest Fixtures
# tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from tests.factories import UserFactory, ProductFactory, OrderFactory
@pytest.fixture(scope='session')
def engine():
"""Create database engine."""
return create_engine('sqlite:///:memory:')
@pytest.fixture(scope='session')
def tables(engine):
"""Create all tables."""
Base.metadata.create_all(engine)
yield
Base.metadata.drop_all(engine)
@pytest.fixture
def db_session(engine, tables):
"""Create database session for each test."""
Session = sessionmaker(bind=engine)
session = Session()
yield session
session.rollback()
session.close()
@pytest.fixture
def sample_users(db_session):
"""Create sample users for testing."""
users = UserFactory.build_batch(5)
db_session.add_all(users)
db_session.commit()
return users
@pytest.fixture
def sample_products(db_session):
"""Create sample products for testing."""
products = ProductFactory.build_batch(10)
db_session.add_all(products)
db_session.commit()
return products
@pytest.fixture
def admin_user(db_session):
"""Create an admin user."""
admin = UserFactory.build(admin=True)
db_session.add(admin)
db_session.commit()
return admin
@pytest.fixture
def order_with_items(db_session, sample_users, sample_products):
"""Create an order with items."""
order = OrderFactory.build(
user=sample_users[0],
products=sample_products[:3]
)
db_session.add(order)
db_session.commit()
return order
# Usage in tests
def test_user_orders(order_with_items):
"""Test user has correct orders."""
user = order_with_items.user
assert len(user.orders) == 1
assert user.orders[0].id == order_with_items.id
4. Realistic Data Generation
// tests/helpers/dataGenerator.js
const { faker } = require('@faker-js/faker');
class DataGenerator {
static generateCreditCard() {
return {
number: faker.finance.creditCardNumber('#### #### #### ####'),
cvv: faker.finance.creditCardCVV(),
expiry: faker.date.future().toISOString().slice(0, 7), // YYYY-MM
type: faker.helpers.arrayElement(['visa', 'mastercard', 'amex']),
};
}
static generateAddress() {
return {
street: faker.location.streetAddress(),
city: faker.location.city(),
state: faker.location.state(),
zip: faker.location.zipCode(),
country: faker.location.country(),
coordinates: {
lat: parseFloat(faker.location.latitude()),
lng: parseFloat(faker.location.longitude()),
},
};
}
static generateDateRange(days = 30) {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
return { startDate, endDate };
}
static generateTimeSeries(count, interval = 'day') {
const data = [];
const now = new Date();
for (let i = count - 1; i >= 0; i--) {
const date = new Date(now);
if (interval === 'day') date.setDate(date.getDate() - i);
if (interval === 'hour') date.setHours(date.getHours() - i);
data.push({
timestamp: date,
value: faker.number.float({ min: 0, max: 100, precision: 0.01 }),
});
}
return data;
}
static generateRealisticEmail(firstName, lastName, domain = 'example.com') {
const patterns = [
`${firstName}.${lastName}`,
`${firstName}${lastName}`,
`${firstName.charAt(0)}${lastName}`,
`${firstName}_${lastName}`,
];
const pattern = faker.helpers.arrayElement(patterns);
return `${pattern.toLowerCase()}@${domain}`;
}
}
module.exports = { DataGenerator };
Best Practices
✅ DO
- Use faker libraries for realistic data
- Create reusable factories for common objects
- Make factories flexible with overrides
- Generate unique values where needed (emails, IDs)
- Use builders for complex object construction
- Create fixtures for integration test setup
- Generate edge cases (empty strings, nulls, boundaries)
- Keep test data deterministic when possible
❌ DON'T
- Hardcode test data in multiple places
- Use production data in tests
- Generate truly random data for reproducible tests
- Create overly complex factory hierarchies
- Ignore data relationships and constraints
- Generate massive datasets for simple tests
- Forget to clean up generated data
- Use the same test data for all tests
Tools & Libraries
- JavaScript: @faker-js/faker, fishery, rosie, casual
- Python: factory_boy, faker, hypothesis
- Java: Instancio, EasyRandom, JavaFaker, Mockito
- Ruby: FactoryBot, Faker, Fabrication
- Database: SQL fixtures, JSON fixtures, CSV imports
Example: Complete Test Data Setup
// tests/setup/testData.ts
import { faker } from '@faker-js/faker';
// Configure faker for deterministic tests
faker.seed(12345);
export const TestData = {
users: {
admin: () => ({
email: 'admin@test.com',
role: 'admin',
permissions: ['read', 'write', 'delete'],
}),
regular: () => ({
email: faker.internet.email(),
role: 'user',
isActive: true,
}),
},
products: {
inStock: (overrides = {}) => ({
name: faker.commerce.productName(),
price: parseFloat(faker.commerce.price()),
stock: faker.number.int({ min: 10, max: 100 }),
isAvailable: true,
...overrides,
}),
outOfStock: () => ({
...TestData.products.inStock(),
stock: 0,
isAvailable: false,
}),
},
orders: {
pending: (userId: string) => ({
userId,
status: 'pending',
items: [],
total: 0,
}),
completed: (userId: string) => ({
userId,
status: 'completed',
completedAt: faker.date.recent(),
items: [],
total: faker.number.float({ min: 10, max: 1000 }),
}),
},
};
Examples
See also: integration-testing, mocking-stubbing, continuous-testing skills for using test data effectively.