Writing good tests

Keep tests independent of each other

Tests should pass alone, in any order and in parallel. Find order-dependent pytest tests with pytest-randomly and pytest-xdist, and fix the shared state behind them.

An isolated test passes when you run it alone, when you run it after any other test, and when other tests run at the same time. A test that only passes in the order your suite happens to run today is a flaky test waiting for a trigger: a new test file, a renamed module, pytest -n auto, or a teammate running one file to debug it.

Order-dependent failures are miserable to debug because the broken test isn't the one that fails. Test A leaves something behind, test B trips over it, and B's traceback says nothing about A.

Three rules

  1. Every test passes alone. pytest tests/test_orders.py::test_refund must work without running anything else first.
  2. Every test passes in any order. No test relies on state another test created or cleaned up.
  3. Every test passes in parallel. Two tests running at the same time on different workers don't share anything they both write to.

Where coupling hides

Anything that outlives a test can couple it to the next one.

Shared thing How it leaks Fix
Database rows Data created outside the per-test transaction (at module import, in setUpClass, in session fixtures that commit, e.g. through pytest-django's django_db_blocker) Create data inside the test's transaction: TestCase + setUpTestData, or a rollback fixture (SQLAlchemy)
Hardcoded IDs get(pk=1) only works if this test's row happened to get id 1 Use the object's .pk; never assume sequence values
Module globals A test sets config.FEATURE = True and never resets it monkeypatch.setattr or override_settings
Caches functools.lru_cache, Django's cache, a memoized client hold values from an earlier test cache_clear() / cache.clear() in an autouse fixture
Singletons and registries Signal receivers, plugin registries, a lazily created client configured by the first test that uses it Undo registrations in teardown; build the object per test or reset it
Environment variables os.environ["KEY"] = ... stays for the rest of the run monkeypatch.setenv / delenv
Files Writing to the working directory or a fixed /tmp path tmp_path
Time Code compares against the real clock; the test passes until midnight or month end time-machine with a fixed time (freezegun to time-machine)
Random data The test passes for most generated values, or depends on the sequence random produced in earlier tests Fix the seed per test (pytest-randomly does) and make edge cases explicit
Patches without undo mock.patch(...).start() without stop(), or assigning a fake to a module attribute with mock.patch(...), the decorator form, or monkeypatch
Threads and tasks A background thread or asyncio task keeps running into the next test Stop and join them in teardown

The common thread: every change to shared state needs an undo that runs even when the test fails. pytest's monkeypatch and tmp_path, Django's override_settings and TestCase, and context managers all give you that. pytest fixtures best practices covers fixtures that clean up after themselves.

Shuffle the order with pytest-randomly

pytest runs tests in file order, so the same accidental dependencies keep working. pytest-randomly shuffles the order on every run (modules, then classes, then the tests within them) and resets random.seed() before each test, so hidden dependencies surface:

$ uv add --dev pytest-randomly
$ pytest
Using --randomly-seed=1553614239
...

It also reseeds factory_boy, Faker, model_bakery and NumPy's legacy global random state at the start of each test, so generated data is reproducible from the seed. When a run fails, rerun with the same seed to get the same order and the same data:

$ pytest --randomly-seed=1553614239
$ pytest --randomly-seed=last        # the previous run's seed (needs pytest's cache)

Other switches:

  • -p no:randomly turns the plugin off for one run, for example to check that a failure is order-related.
  • --randomly-dont-reorganize keeps the order but still resets the seed per test.
  • --randomly-dont-reset-seed shuffles without touching random.

If you run pytest with plugin autoloading disabled (PYTEST_DISABLE_PLUGIN_AUTOLOAD=1), load it explicitly with -p pytest_randomly.

Django's own runner has the same idea built in: manage.py test --shuffle (with an optional seed to repeat an order) and --reverse, which runs tests in the opposite order.

Use pytest-xdist as an isolation check

Running in parallel catches a second class of problems: tests that pass in any order but break when they run at the same time.

$ pytest -n 4

With pytest-xdist each worker is a separate process with its own globals, and tests are spread across workers differently on each run, so a test that relied on another test running earlier in the same process starts failing. Tests that share something outside the process fail too: a fixed file path, a fixed port, a Redis database, a row with a fixed unique name in a shared database. pytest-django gives each worker its own test database; for everything else, namespace by worker with the worker_id fixture (or the PYTEST_XDIST_WORKER environment variable) or use tmp_path and port 0. Run tests in parallel with pytest-xdist has the setup.

Run one test alone

The cheapest check is the one people forget. A test that only passes after the rest of its file has run depends on something an earlier test (or a module-level side effect) set up:

$ pytest "tests/test_orders.py::TestRefunds::test_partial_refund"

When a test fails in the full run but passes alone, something earlier polluted it. When it passes in the full run but fails alone, it depends on something earlier. Either way the next step is to find the other test.

Find the test that pollutes another

Bisecting by hand is slow. detect-test-pollution automates it: give it the failing test and the suite, and it repeatedly runs halves of the tests before it until it finds the one that causes the failure:

$ uv add --dev detect-test-pollution
$ detect-test-pollution --failing-test tests/test_orders.py::test_refund --tests tests/

A pytest-randomly seed that reproduces the failure gives you a fixed order to work with: rerun with --randomly-seed=<seed> and the polluter is one of the tests that ran before the failing one in the same process.

Order dependence looks like flakiness

A test that depends on order often looks flaky: it passes on one CI run and fails on the next, on the same commit, because something else changed the order (pytest-randomly, xdist distribution, a new test). On flakytest.dev a failure streak where a test both failed and passed on the same commit is classified as flaky, so order-dependent tests show up there alongside timing problems. Find flaky tests and common causes of flaky tests cover the other causes and how to tell them apart.

Isolation and speed pull in different directions

Wider fixture scopes, setUpTestData and a shared browser all make tests faster by sharing things, and every shared thing is a chance to leak. That's not a reason to avoid them, but share read-only things, give each test a cheap reset (a rollback, a fresh browser context, flushdb()), and keep pytest-randomly on so leaks show up the day they're introduced. Expensive function-scoped pytest fixtures shows how to widen a scope without sharing mutable state.

Further reading

Related guides

  • Common causes of flaky tests and how to fix each: Why pytest tests pass and fail on the same code: test order, time zones, randomness, sleeps, unordered results, xdist and slow CI. The symptom and fix for each.
  • pytest fixtures best practices: How to write pytest fixtures that stay fast and readable: scopes, yield teardown, factories as fixtures, conftest.py placement, autouse, parametrize, tmp_path and monkeypatch.
  • Run tests in parallel with pytest-xdist: Run pytest in parallel on every CPU with pytest-xdist and -n auto. Per-worker databases with pytest-django, what breaks in parallel, pytest-randomly, and the --dist modes.
  • How to find flaky tests: Find and reproduce flaky pytest tests: track results across runs, rerun on the same commit, pytest-randomly seeds, pytest-repeat, xdist stress and CI-like CPU limits.

Updated October 18, 2026 · Suggest a change