Guides
Fast, reliable test suites
What slows Python test suites down and what makes them flaky, with the fix for each. Every guide is also available as Markdown for coding agents (see llms.txt).
Speed up your test suite
Every slowdown flakytest detects in a pytest suite: why it's slow, how to fix it, and how to keep the tests correct while you do.
How to speed up a slow pytest suite
A checklist for slow Python test suites, from the fixes that usually save the most time (password hashing, migrations, test data) to parallelism and CI. Each item links to a detailed guide.
Use a fast password hasher in tests
Django hashes passwords with hundreds of thousands of PBKDF2 iterations, and every create_user and client.login in a test pays for it. Switch to a fast hasher in test settings and use force_login.
Stop re-running migrations on every test run
Django runs every migration before the first test, on every run. Keep the test database with --reuse-db or --keepdb, or skip migrations with --nomigrations.
Run migrations once with pytest-xdist and CI shards
With pytest-xdist every worker builds its own Django test database, and every CI shard does too. Migrate once and copy it with PostgreSQL template databases or pg_dump.
Use TestCase instead of TransactionTestCase
TransactionTestCase and transaction=True empty every table after each test. Django's TestCase rolls back a transaction instead. When to use which, and how to switch.
Create shared test data in setUpTestData instead of setUp
Django runs setUp before every test, so its rows are inserted again each time. setUpTestData creates them once per TestCase class, and every test still starts clean.
Load Django fixtures once per class, not once per test
Django fixtures (loaddata) save every object one at a time. Load them once per TestCase class or pytest session instead of before every test, or use factories.
setUpTestData deep copies and expensive copy.deepcopy
Django deep-copies attributes set in setUpTestData for every test that reads them. Big lists, prefetched instances and querysets make it slow. What to store instead.
Create the SQLAlchemy schema once, roll back per test
Running metadata.create_all and drop_all for every test is slow. Create the SQLAlchemy schema once per session and roll back each test with join_transaction_mode="create_savepoint".
Expensive function-scoped pytest fixtures
A pytest fixture without a scope runs again for every test that uses it. Give expensive fixtures module or session scope, or split them into a shared part and a cheap reset.
Faster factory_boy and model_bakery factories
Slow factory_boy and model_bakery factories? Create shared data once, use build() or baker.prepare() instead of saving, bulk create rows, reuse one Faker instance and tune Hypothesis.
Find and fix N+1 queries with your tests
N+1 queries run one query per row. Fix them with select_related/prefetch_related or SQLAlchemy's selectinload/joinedload, and lock the fix in with assertNumQueries.
Stop opening new database connections in tests
Tests that create an engine per test, use NullPool or close Django connections reconnect to the database over and over. Create the engine once per session and keep connections open.
Make the test PostgreSQL/MySQL server skip durability work
Speed up database-heavy tests by running the test PostgreSQL or MySQL server with fsync=off, synchronous_commit=off and a tmpfs data directory, in Docker or GitHub Actions.
Tests are sleeping: replace time.sleep
Fixed time.sleep and asyncio.sleep calls make pytest suites slow and flaky. Poll for the condition, patch the sleep in retry and backoff code, and move the clock with time-machine.
Tests blocked on threads, queues and the event loop
Tests that wait in Thread.join, Event.wait, Queue.get or select() burn time doing nothing. Lower timeouts in tests, signal completion, and run background work synchronously.
Mock and block real network calls in tests
Real HTTP calls make pytest suites slow and flaky. Mock at the transport with responses, respx, pytest-httpx, aioresponses, moto or vcrpy, then block the network with pytest-socket.
Use an in-memory cache in tests
Tests that talk to a real Redis or Memcached pay a network round trip per cache call and share state between tests. Use Django's LocMemCache or fakeredis in tests and clear it per test.
Run Celery tasks eagerly in tests
Tests that call task.delay() publish to a broker, retry against a missing one or wait on a worker. Run tasks eagerly with CELERY_TASK_ALWAYS_EAGER, or mock delay.
Django test settings for speed (debug toolbar, DEBUG, static storage)
Development settings slow Django tests: keep django-debug-toolbar out, don't force DEBUG = True, and use StaticFilesStorage instead of manifest or WhiteNoise storage.
Tests that spawn processes
Tests that run CLIs with subprocess or start multiprocessing pools and Docker containers pay for a new process each time. Call code in-process, fake subprocess.run, share containers.
Share the browser between Playwright and Selenium tests
Launching a browser for every test is slow. Use pytest-playwright's session-scoped browser with a new context per test, or a session-scoped Selenium driver, and a separate CI job.
Replace freezegun with time-machine
freezegun scans every loaded module each time it freezes time, which gets slow in big projects. time-machine mocks time at the C level. How to migrate, including tick=False.
Expensive mock setup (unittest.mock, autospec)
mock.patch with autospec=True, create_autospec and deep MagicMock chains cost real time when repeated in every test. Patch once per class or module and reuse spec'd mocks.
One event loop per async test is slow
pytest-asyncio gives every async test its own event loop by default. Share one loop with asyncio_default_test_loop_scope and loop_scope="session" so async fixtures can be shared too.
Fix slow pytest startup and heavy imports in tests
pytest takes seconds before the first test runs? Find slow imports with python -X importtime, set testpaths, trim conftest.py imports and disable plugins you don't use.
Garbage collection pauses in tests (gc.freeze)
Python's cyclic garbage collector rescans every long-lived object in a big test suite. Call gc.freeze() after collection and raise gc thresholds to cut GC pauses in pytest.
Logging is expensive in tests
DEBUG logging in tests creates, formats and captures a record for every log call. Raise the log level in test settings, drop extra handlers, use caplog only where needed.
JSON, YAML and pydantic serialization in tests
Tests that parse the same JSON or YAML files again and again, or validate pydantic models in loops, burn CPU. Parse once, use the C YAML loader, reuse TypeAdapters.
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.
pytest-xdist workers finish at different times (--dist worksteal)
When some pytest-xdist workers sit idle while one still runs tests, the run takes as long as the busiest worker. Use --dist worksteal and split oversized modules or classes.
Faster coverage with sys.monitoring (COVERAGE_CORE=sysmon)
pytest-cov makes tests slower because coverage.py traces every line. On Python 3.12+ set COVERAGE_CORE=sysmon to measure with sys.monitoring at much lower overhead.
Writing good tests
Habits that keep Django, SQLAlchemy and end-to-end test suites fast, isolated and trustworthy as they grow.
Django testing best practices
How to write fast, reliable Django tests: TestCase vs TransactionTestCase, setUpTestData, model_bakery, force_login, test settings, assertNumQueries and parallel runs.
SQLAlchemy testing best practices
How to test SQLAlchemy 2.0 code with pytest: schema once per session, savepoint rollback per test, factory_boy factories, statement counting, AsyncSession and Alembic checks.
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.
End-to-end testing best practices
Fast, reliable end-to-end tests: give every test a fresh tenant instead of cleaning the database, set up state through the API, use Playwright locators and auto-waiting.
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.
Flaky tests
What makes tests flaky, why retrying them isn't a fix, and how to find, mute and fix them.
What is a flaky test?
A flaky test passes and fails on the same code. Why flaky tests are expensive (ignored red builds, missed bugs, CI time) and how to detect them across runs.
Retrying flaky tests isn't a fix
pytest-rerunfailures --reruns and CI job retries hide flaky tests instead of fixing them. Why retries help less than the math suggests, cost CI time and bury real bugs.
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.
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.
Mute flaky tests instead of skipping or retrying them
Mute a flaky pytest test so it can't fail the build while it keeps running and reporting. How muting compares to pytest.mark.skip, xfail, quarantine jobs and retries.
Measure your test suite
How to find out where a pytest suite spends its time, locally and in CI.
How to profile a pytest suite
Profile slow pytest tests with cProfile, pytest-profiling, py-spy, pyinstrument, Python 3.15's tachyon or flakytest: the commands, what each one shows, and which to use when.
Find your slowest pytest tests
List the slowest pytest tests with --durations and --durations-min, read setup vs call vs teardown times, then fix, split or mark slow tests so they stop holding up the suite.
Speed up pytest in CI
Speed up slow CI test jobs: cache dependencies with uv or pip, use pytest-xdist on every core, shard with pytest-split, migrate once, and see why CI is slower than local.
py-spy vs tachyon: benchmarking sampling profilers on pytest suites
We profiled pytest suites with py-spy and Python 3.15's tachyon at different settings. Non-blocking sampling silently drops up to 90% of samples, and the ones it keeps are biased.