Integration & Data

Generating Reproducible Fake Data with Faker

Faker makes test data look realistic, and realistic data finds bugs that "test" and "foo" never do: the apostrophe in O'Brien, the non-ASCII character in Zoë, the email that is valid and ninety characters long. It also makes tests fail at random, because the generator reaches those values only occasionally. Seeding it correctly keeps the first property and removes the second.

Prerequisites

  • Faker >= 25, or the factory.Faker wrapper that ships with factory_boy.
  • pytest >= 8.0.
  • A clear line between values the test asserts on and values it merely needs to exist — see test data factories and builders.

Solution

Print a session seed, derive a stable per-test seed from it, and hand each test a seeded instance.

Python
# conftest.py
import hashlib
import os
import random

import pytest
from faker import Faker


def pytest_addoption(parser):
    parser.addoption("--faker-seed", type=int, default=None,
                     help="reproduce a run's generated data")


@pytest.fixture(scope="session")
def faker_session_seed(request):
    seed = request.config.getoption("--faker-seed")
    if seed is None:
        seed = int(os.environ.get("FAKER_SEED", random.randrange(2**31)))
    # Printed in the header so every failure report carries it.
    print(f"\nFaker session seed: {seed}  (re-run with --faker-seed={seed})")
    return seed


@pytest.fixture
def fake(request, faker_session_seed):
    instance = Faker("en_GB")                     # pinned locale, not the machine's
    # Per-test seed: stable no matter which tests ran before this one.
    digest = hashlib.sha256(f"{faker_session_seed}:{request.node.nodeid}".encode())
    instance.seed_instance(int.from_bytes(digest.digest()[:8], "big"))
    yield instance
    instance.unique.clear()                        # unique pools are per test
Python
def test_customer_round_trips_through_the_api(api, fake):
    payload = {
        "name": fake.name(),                     # realistic, never asserted on
        "email": fake.unique.email(),            # unique within this test
        "vat_exempt": True,                      # asserted on: explicit
    }
    created = api.post("/customers", json=payload).json()
    assert created["vat_exempt"] is True
    assert created["name"] == payload["name"]    # round trip, not a literal
Session seed versus per-test seed Two arrangements. With one shared seed, each test's data depends on how many values earlier tests consumed, so inserting a test changes the data of every test after it. With a per-test seed derived from the session seed and the node id, each test's data is fixed regardless of order or of tests added elsewhere. Where each test's random stream starts one shared seed test A consumes values 1–5 test B gets values 6–9 insert a test before B → B now gets different data failures move when tests are added seed per test seed = hash(session, nodeid) each test starts its own stream order and neighbours irrelevant -k one_test reproduces exactly a failure stays where it is
The per-test derivation is what makes pytest -k failing_test --faker-seed=N reproduce the failing data. With a shared seed, running one test alone gives it different values.

Why this works

Faker.seed_instance resets an instance's private random generator, so every value it produces afterwards follows a deterministic sequence. Hashing the session seed together with the node id gives each test its own starting point that depends only on those two inputs — not on how many values earlier tests consumed, not on whether the test was selected with -k, not on pytest-randomly shuffling the order.

The printed seed is the half that makes this useful in CI. A failure caused by generated data carries the seed in its header, and re-running with --faker-seed regenerates exactly the same values for exactly that test.

Edge cases and failure modes

  • Faker.seed() versus seed_instance(). The class method seeds a shared generator used by every instance, so two fixtures seeding it race each other. Always seed the instance.
  • unique exhaustion. fake.unique.first_name() raises UniquenessException once the pool is used up. Clear it per test, and prefer sequences for anything that needs thousands of unique values.
  • Locale drift. Faker() with no argument uses a default locale, and output formats differ between locales. Pin it.
  • Faker version upgrades. Provider word lists change between releases, so the same seed produces different values after an upgrade. That is fine as long as nothing asserts on a generated literal.
  • Generated values in assertions. assert customer.name == "Gemma Hughes" passes until the next Faker release. Assert on round trips or on values the test supplied.

Deciding which values may be generated

The rule "never assert on generated values" is easy to state and easy to break by accident, so it helps to classify every field a test touches before deciding where its value comes from.

Asserted fields are the ones the test's outcome depends on: the amount that must be refunded, the status that must change, the flag that must be respected. These are always passed explicitly, in the test, where the reader can see them.

Incidental fields are required for the object to be valid but play no part in the behaviour: a customer's name on an order-total test, an address on a VAT test. These are the natural home for generated data, and generating them is actively useful because it exercises the code with varied realistic input on every run.

Constrained fields must satisfy a rule the generator does not know about: a unique email, a postcode matching a country, a date after another date. These need either a unique proxy, a derivation from another field, or an explicit value — never a bare generator call.

Python
def test_refund_covers_the_full_amount(api, fake):
    order = an_order(
        amount_minor=4_999,                  # asserted: explicit
        customer_name=fake.name(),           # incidental: generated
        customer_email=fake.unique.email(),  # constrained: unique proxy
        country="GB",
        postcode=fake.postcode(),            # constrained: locale-pinned to GB
    )
    refund = api.refund(order.id)
    assert refund.amount_minor == 4_999
Three kinds of field and where each value comes from Asserted fields, which the outcome depends on, are passed explicitly. Incidental fields, which must be valid but play no part in the behaviour, are generated. Constrained fields, which must satisfy a rule the generator does not know, use a unique proxy, a derivation or an explicit value. Classify first, then choose the source asserted outcome depends on it amount, status, flags always explicit visible in the test incidental must be valid, plays no part in behaviour generate freely variety finds bugs constrained unique, derived or locale-dependent unique proxy or derive never a bare call
Most flaky Faker usage is a constrained field treated as incidental — a bare fake.email() on a column with a unique index.

Writing the classification into the test, as the comments above do, costs nothing and makes review straightforward: a reviewer can see at a glance whether any asserted field is being generated, which is the one mistake that turns realistic data into a flaky test.

The same classification also guides upgrades. When a new Faker release changes a provider's output, only tests that were quietly depending on generated values fail — and every one of those is a test that had an asserted or constrained field mis-classified as incidental. Treating an upgrade's failures as a list of classification bugs to fix, rather than as a reason to pin the old version, leaves the suite better than it was and keeps the dependency current. Pinning Faker to avoid those failures, by contrast, simply defers them to a larger and more confusing upgrade later, when many more tests have accumulated the same hidden dependency on specific generated strings.

Making generated data find bugs on purpose

Realistic data is most valuable when it is steered toward the values that break code. Faker's providers accept arguments and can be combined with a small amount of deliberate edge-case mixing.

Python
import random


def awkward_name(fake: "Faker", rng: random.Random) -> str:
    # Mostly realistic, sometimes deliberately hostile.
    return rng.choice([
        fake.name(),
        fake.name(),
        "O'Brien",                 # apostrophe: SQL and CSV escaping
        "Zoë Ångström",            # non-ASCII: encoding and collation
        "李小龙",                    # CJK: width and sorting
        "A" * 255,                 # length limit
    ])

Seeding rng from the same per-test seed keeps this reproducible. The mix finds encoding and escaping bugs within the first few runs, and because the seed is printed, a failure on the CJK name reproduces exactly.

This is as far as Faker should be pushed. When the goal becomes systematically exploring which inputs break a function — rather than filling fields with plausible values — the right tool is property-based testing with Hypothesis, which searches the input space deliberately and shrinks a failure to its minimal cause.

Seeding factory_boy's Faker too

Suites that use factory.Faker in factory declarations have a second generator to control, because factory_boy keeps its own Faker instances rather than using the one a fixture provides.

Python
# conftest.py
import factory.random
import pytest


@pytest.fixture(autouse=True)
def seed_factories(request, faker_session_seed):
    digest = hashlib.sha256(f"{faker_session_seed}:{request.node.nodeid}".encode())
    # Seeds factory_boy's shared random state, which its Faker attributes and
    # fuzzy attributes both draw from.
    factory.random.reseed_random(int.from_bytes(digest.digest()[:8], "big"))

Reusing the same derivation as the fake fixture means one printed session seed reproduces both generators for any test, which matters because a failure caused by a factory-generated name looks identical to one caused by a fixture-generated name in the report.

One session seed controlling two generators A single printed session seed is combined with the test's node id to derive a per-test seed. That seed is applied both to the fake fixture's Faker instance and to factory_boy's shared random state, so re-running with the same session seed reproduces every generated value in the test regardless of which mechanism produced it. One seed in the report reproduces everything session seed printed in the header hash(seed, nodeid) per-test seed fake fixture seed_instance() factory_boy reseed_random()
Seeding only one of the two leaves half the generated data random, which is enough to make a failure irreproducible.

Under pytest-xdist the arrangement needs no change: each worker computes the same per-test seed for the same node id, so a failure on worker three reproduces locally with the printed seed and -k alone. That independence from execution topology is the property a shared, sequential seed can never have, and it is the reason to prefer the derivation over the simpler global Faker.seed(n) that most tutorials show.

Frequently Asked Questions

Why does a test using Faker fail only occasionally? Because the generator produced a value the code cannot handle — an apostrophe in a name, a duplicate email, a date on a boundary — and it does so only when the random sequence happens to reach it. Seed the generator, print the seed, and the failing run becomes reproducible on demand.

Should Faker be seeded once per session or once per test? Per test, derived from a session seed and the test's node id. A single session seed makes each test's data depend on how many values earlier tests consumed, so adding or reordering tests changes every later test's input. A per-test seed keeps each test's data stable regardless of the rest of the suite.

Is Faker a substitute for property-based testing? No. Faker produces realistic-looking values for filling fields; it does not search for failing inputs or shrink a failure to a minimal case. When the goal is to find the inputs that break the code, Hypothesis is the tool, and Faker remains useful for readable fixtures and demo data.

← Back to Test Data Factories & Builders