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 thefactory.Fakerwrapper that ships withfactory_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.
# 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
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
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()versusseed_instance(). The class method seeds a shared generator used by every instance, so two fixtures seeding it race each other. Always seed the instance.uniqueexhaustion.fake.unique.first_name()raisesUniquenessExceptiononce 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.
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
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.
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.
# 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.
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.
Related
- Test Data Factories & Builders — where Faker-backed factory attributes fit.
- factory_boy versus Plain Fixture Builders — choosing the mechanism that calls Faker.
- Seeding random and NumPy for Reproducible Tests — the same discipline for other generators.
- Designing Strategies for Domain Data — when generation should search rather than fill.
← Back to Test Data Factories & Builders