StackPractices
beginner By Mathias Paulenko

Generate Test Data (Faker + Factories)

How to generate realistic, deterministic test data with Faker, factory-boy, and type-aware generators for reliable test suites in Python, JavaScript, and Java.

Topics: testing

Overview

Hardcoded test data (name = "John", email = "test@test.com") goes stale fast, misses edge cases, and looks nothing like production distributions. Generators produce realistic, varied, deterministic data — tests get more reliable while fixture maintenance shrinks.

When to Use

  • You maintain dozens of hardcoded test objects that drift from the production schema
  • Edge cases (empty strings, Unicode, very long values) never get tested because they’re tedious to write
  • Integration tests need a database seeded with hundreds of realistic rows
  • You want tests to exercise validation rules with varied input distributions
  • Load tests need large volumes of plausible data

If the fixtures themselves are the problem — shared setup, teardown, database state — pair this with test fixture setup.

When NOT to Use

  • The test needs one very specific, known scenario — hardcode it explicitly
  • Determinism matters more than variety — seed the generator but keep values minimal
  • The schema is trivial (2-3 fields) — a literal object is clearer
  • You’re testing a Faker-like library itself — use controlled, predictable inputs

Step-by-Step Implementation

Python

from faker import Faker
from dataclasses import dataclass
from typing import List
import factory
from factory import Faker as FactoryFaker

fake = Faker()
Faker.seed(12345)  # Deterministic across runs

# Basic Faker usage
fake.name()        # 'John Smith'
fake.email()       # 'john.smith@example.com'
fake.ipv4()        # '192.168.1.45'
fake.uuid4()       # '550e8400-e29b-41d4-a716-446655440000'

# factory-boy for ORM objects
@dataclass
class User:
    id: int
    name: str
    email: str
    age: int
    is_active: bool

class UserFactory(factory.Factory):
    class Meta:
        model = User

    id = factory.Sequence(lambda n: n)
    name = FactoryFaker('name')
    email = FactoryFaker('email')
    age = factory.Faker('random_int', min=18, max=90)
    is_active = True

# Usage
user = UserFactory()                  # Single instance
users = UserFactory.build_batch(100)  # 100 instances
admin = UserFactory(name="Admin User", age=30)

# Custom provider for domain-specific data
from faker.providers import BaseProvider

class ProductProvider(BaseProvider):
    def sku(self):
        categories = ['ELEC', 'BOOK', 'HOME', 'TOY']
        return f"{self.random_element(categories)}-{self.random_int(1000, 9999)}"

fake.add_provider(ProductProvider)
fake.sku()  # 'ELEC-4521'

# Deterministic dataset for property-based tests
import hypothesis.strategies as st

user_strategy = st.builds(
    User,
    id=st.integers(min_value=1),
    name=st.text(min_size=1, max_size=100),
    email=st.emails(),
    age=st.integers(min_value=0, max_value=120),
    is_active=st.booleans()
)

JavaScript

import { faker } from '@faker-js/faker';

// Seed for determinism
faker.seed(12345);

// Basic generators
faker.person.fullName();    // 'John Smith'
faker.internet.email();     // 'john.smith@example.com'
faker.number.int({ min: 18, max: 65 });  // 34

// Factory function
function createUser(overrides = {}) {
    return {
        id: faker.string.uuid(),
        name: faker.person.fullName(),
        email: faker.internet.email(),
        age: faker.number.int({ min: 18, max: 90 }),
        avatar: faker.image.avatar(),
        isActive: true,
        ...overrides
    };
}

// Generate batch
const users = Array.from({ length: 100 }, () => createUser());

// Domain-specific faker helpers
const createOrder = (overrides = {}) => ({
    id: faker.string.uuid(),
    customerId: faker.string.uuid(),
    items: Array.from({ length: faker.number.int({ min: 1, max: 5 }) }, () => ({
        sku: `SKU-${faker.string.alphanumeric(6).toUpperCase()}`,
        quantity: faker.number.int({ min: 1, max: 10 }),
        price: faker.commerce.price({ min: 5, max: 500 })
    })),
    status: faker.helpers.arrayElement(['pending', 'paid', 'shipped', 'delivered']),
    createdAt: faker.date.past(),
    ...overrides
});

// Deterministic data for snapshots
faker.seed(42);
const snapshotUser = createUser({ name: 'Snapshot User' });

Java

import net.datafaker.Faker;
import java.util.List;
import java.util.stream.IntStream;

public class TestDataGenerator {
    private static final Faker faker = new Faker();

    public static User createUser() {
        return User.builder()
            .id(faker.number().randomNumber())
            .name(faker.name().fullName())
            .email(faker.internet().emailAddress())
            .age(faker.number().numberBetween(18, 90))
            .isActive(true)
            .build();
    }

    public static List<User> createUsers(int count) {
        return IntStream.range(0, count)
            .mapToObj(i -> createUser())
            .toList();
    }

    // JUnit 5 parameterized with generated data
    public static Stream<Arguments> emailProvider() {
        return Stream.generate(() -> Arguments.of(faker.internet().emailAddress()))
            .limit(50);
    }
}

// Instancio for type-aware generation
import org.instancio.Instancio;
import org.instancio.Select;

User user = Instancio.of(User.class)
    .set(Select.field("role"), "admin")
    .generate(Select.field("age"), gen -> gen.ints().range(18, 90))
    .create();

List<User> users = Instancio.ofList(User.class).size(100).create();

Choosing an Approach

Four tools cover most needs, and they solve different problems:

flowchart diagram: Need test data

Faker alone gives you realistic field values but no structure. Factories build whole objects (or ORM rows) with sane defaults plus per-test overrides. Property-based generators like Hypothesis explore input space for you and shrink failures to minimal cases — reach for them when you’re asserting invariants, not specific scenarios. The property-based testing recipe goes deeper on that path.

ApproachBest forTools
Hardcoded literalOne exact scenario, snapshot/golden testsNone — just write the object
Seeded FakerRealistic field values, validation fuzzingfaker (Py), @faker-js/faker, DataFaker
FactoryWhole objects, ORM rows, per-test overridesfactory-boy, Instancio, factory functions
Property-basedInvariants, edge-case discovery, shrinkinghypothesis, fast-check

Most mature suites end up combining two or three: factories for the object graph, Faker inside them for field values, and a seeded baseline so every run sees the same data.

What Works

  • Always seed your random generator. Without a seed, a test that fails on CI may pass locally because the data differed. Set Faker.seed() or faker.seed() in a global setup file.
  • Override specific fields for scenario tests: createUser({ role: 'admin' }) beats hoping the random generator happens to produce an admin.
  • Constrain ranges to the domain. A random age between 0 and 120 mostly produces invalid data; 18-90 fits adult users.
  • Generate data close to the test. A shared users.json fixture drifts from the schema; programmatic generation updates every test when a field is added.
  • Include edge cases on purpose: empty strings, max lengths, Unicode, and nulls alongside happy-path generated data.

Common Mistakes

  • Unseeded random data — tests fail intermittently because a random email hit a uniqueness constraint, or a random string accidentally looked like SQL injection.
  • Overly permissive ranges — faker.number.int() defaults are wide enough to violate business rules (negative prices, 200-character names).
  • Mixing generated and hardcoded data inconsistently — half the suite uses Faker, half uses literals, and nobody knows which to reach for.
  • Exporting a JSON fixture once and never regenerating it — stale data never exercises validation rules added after the export.
  • Generators that depend on each other — createOrder() calling createUser() internally hides the user and makes relationship assertions impossible.

See Also

Companion code: the runnable generators for all three languages live in the companion repo.

Frequently Asked Questions

Why should I use factories instead of static fixtures?

Factories generate data on demand and adapt to schema changes automatically. A users.json file checked into git goes stale and stops exercising new validation rules. Keep static fixtures only for golden-path tests where exact data matters, like snapshots.

What data should never appear in tests?

Never real personal data, production credentials, or payment details. Synthetic data should look real without exposing anyone — even masked production copies can leak through residual fields.

How do I keep test data deterministic across CI and local runs?

Seed the generator globally: Faker.seed(12345) in conftest.py, faker.seed(12345) in Jest globalSetup, new Faker(new Random(12345)) in Java. Never rely on system time or /dev/urandom — intermittent failures usually mean an unseeded generator hiding in a helper.

How do I generate data with relationships between entities?

Pass related objects explicitly: const user = createUser(); const order = createOrder({ customerId: user.id }). Don't have createOrder() call createUser() internally — it hides the user and breaks relationship assertions. In Python, factory.SubFactory(UserFactory) links factories cleanly.

What edge cases should generated data include?

Cover the boundaries your validators care about: empty strings, maximum lengths, Unicode and emoji, nulls, zero, negative numbers, and dates at limits (epoch, far future). Generate ~80% of cases with Faker and add the edge list explicitly, or let a property-based tool explore them for you.

How do I handle unique constraints with generated data?

Faker doesn't guarantee uniqueness. Append a counter (f"user{n}@example.com"), use factory.Sequence in factory-boy, or prefer uuid() values which are unique by design. Don't rely on luck — collisions get more likely as batch size grows (birthday paradox).

How do I generate data for database integration tests?

Use factory-boy with your ORM: class UserFactory(factory.django.DjangoModelFactory) with Meta: model = User, then UserFactory.create() or create_batch(1000). Wrap tests in a transaction that rolls back so data never accumulates between cases.

Can Faker generate localized data for i18n tests?

Yes — Faker('es_ES'), Faker('ja_JP') produce locale-correct names, addresses, and phone formats. Combine with explicit Unicode cases (CJK, RTL, emoji) and byte-vs-character length checks, since a CJK character can be 3 bytes in UTF-8.

How do I clean up generated test data after tests?

Roll back a transaction per test, or truncate a dedicated test schema between suites. For files, use tempfile directories that the OS cleans up. Never point any of this at production.