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.

Flaky tests

What makes tests flaky, why retrying them isn't a fix, and how to find, mute and fix them.

Measure your test suite

How to find out where a pytest suite spends its time, locally and in CI.