Hey everyone,
As our codebase has grown, our Django test suite execution time has slowed down significantly, and it’s starting to bottleneck our local feedback loops and CI/CD pipelines.
We are already using two major optimization strategies:
-
Running our test cases in 8 parallel processes (--parallel=8).
-
Using FactoryBoy for handling ORM setup more efficiently than standard Django fixtures.
Even with these, the suite is sluggish. We want to implement deeper, structural optimizations. What are your go-to advanced strategies for squeezing extra speed out of a growing Django test suite?
My default would be to recommend Adam Johnson’s book: Speed Up Your Django Tests
Two things from top of my head:
- Skip migrations during test setup (aka don’t let Django execute all migrations one by one, but rather reconstruct final db directly)
- Don’t sleep on durability settings for your database, e.g. for Postgres you can make like 30 % speedup just from good db settings: Less durability = faster tests - by Petr Stribny
Hi how’re you feeling chief?
Great question here are the structural optimizations that usually give the biggest wins on a large Django suite, roughly in order of impact:
- keepdb (huge, often overlooked)
Recreating the test database and re-running all migrations on every run is expensive. Reuse it:
python3 manage.py test --parallel=8 --keepdb
Combine it with disabling migrations entirely for tests build the schema straight from the models instead of replaying migration history:
settings/test.py
class DisableMigrations:
def contains(self, item): return True
def getitem(self, item): return None
MIGRATION_MODULES = DisableMigrations()
- Use a fast password hasher
Argon2/bcrypt are intentionally slow. Any test that creates a user pays that cost:
PASSWORD_HASHERS = [“django.contrib.auth.hashers.MD5PasswordHasher”]
This alone can shave a surprising amount off auth-heavy suites.
- setUpTestData instead of setUp
setUp runs before every test. setUpTestData runs once per class and is wrapped in a transaction rolled back between tests so shared read-only fixtures are created a single time:
class OrderTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.customer = CustomerFactory()
-
Prefer TestCase over TransactionTestCase
TestCase wraps each test in a transaction and rolls back far faster than truncating tables. Only use TransactionTestCase when you genuinely need to test transaction behavior.
-
Tighten FactoryBoy usage
- Use .build() (no DB hit) instead of .create() whenever you don’t need persistence.
- Watch out for SubFactory chains silently creating deep object graphs — pass existing instances instead.
- Use django_get_or_create to avoid redundant inserts.
- Profile before optimizing further
Find the actual offenders rather than guessing:
with pytest-django
pytest --durations=20
or django-slowtests for the vanilla runner. Often 10% of the tests eat 80% of the time.
-
Mock external I/O
Any real network/API/filesystem call in a test is a hidden slowdown (and a flakiness source). Mock them.
-
Split the suite
Fast unit tests for the local loop, slower integration tests gated in CI. Devs get feedback in seconds; the full run happens off the critical path.
One caveat on parallelism: --parallel=8 only helps up to your available cores — going past physical CPUs can slow things down from context-switching, so benchmark it.