Using an Existing Database in Django Tests
Every time you run python manage.py test, Django silently does a lot of things behind the scenes before your first line of test code executes. It creates a new database, runs migrations, wraps each test in a transaction, then deletes everything when done. This mechanism works well for most cases, but once a project grows — hundreds of tests, heavy migrations, or the need for integration tests against real systems — developers start looking for ways to speed up or modify this default behavior. This article discusses how Django truly manages the test database, the official options available to speed it up, and when (and why it’s better not to) use an existing database outside the built-in test cycle.
How the Django Test Database Works
The Django test runner doesn’t directly run your tests against the database configured in DATABASES. It creates a separate copy specifically for testing, usually with a test_ prefix in front of the original database name.
flowchart TD
A[python manage.py test] --> B[Create a new test database]
B --> C[Run all migrations]
C --> D[Execute the test suite]
D --> E[Delete the test database]
E --> F[Show test results]The order is always the same:
- Django reads the
DATABASES['default']configuration and creates a new database namedtest_<original_db_name> - All migrations are run from scratch in this test database
- The test suite executes against a clean, fully migrated database
- After all tests finish, the test database is deleted
This design is intentional so tests are reproducible — anyone running the same tests, on any machine, gets identical results because they always start from the exact same database schema. As a consequence, Django provides no official path to directly run tests against a production or staging database — this is a design decision, not a limitation.
Transaction-Based Test Isolation
Creating a new database for the whole test suite only solves half the problem. The next problem: how to make sure one test doesn’t leave data affecting the next test? This is where transaction wrapping comes in, and it’s a mechanism often under-understood even though it’s the main reason Django tests feel “magical” — data is always clean in every test without you needing to clean it manually.
django.test.TestCase wraps each test method in a database transaction. When the method finishes, the transaction is rolled back, not committed.
sequenceDiagram
participant Runner as Test Runner
participant DB as Test Database
Runner->>DB: BEGIN TRANSACTION
Runner->>DB: test_create_user() - INSERT data
Runner->>DB: assertEqual(...)
Runner->>DB: ROLLBACK
Note over DB: Data is empty again
Runner->>DB: BEGIN TRANSACTION
Runner->>DB: test_update_user() - INSERT, UPDATE
Runner->>DB: assertEqual(...)
Runner->>DB: ROLLBACK
Note over DB: Data is empty againAs a result, you can write tests like this without worrying about data leaking into other tests:
from django.test import TestCase
from myapp.models import Product
class ProductTest(TestCase):
def test_create_product(self):
Product.objects.create(name="Shoes", price=150000)
self.assertEqual(Product.objects.count(), 1)
def test_products_empty_at_start(self):
# Even though the test above created 1 product,
# here Product.objects.count() is still 0
self.assertEqual(Product.objects.count(), 0)
This is why every test method feels like it runs on an “empty database” even though the physical database is the same throughout the test suite — the transaction rollback creates this illusion of isolation.
Transaction rollback doesn’t work for things outside the database transaction, such as:
- Files written to disk
- Caches (Redis, Memcached) unless you reset them manually
- Side effects from
post_savesignals that call external APIs
TestCase vs TransactionTestCase vs SimpleTestCase
Django provides several base classes for tests, and each has different trade-offs regarding speed, isolation, and what features can be tested. Choosing the wrong base class is a source of hard-to-trace test bugs — especially when you’re testing something that depends on a real transaction commit.
| Base Class | Isolation | Speed | When Used |
|---|---|---|---|
SimpleTestCase | No database access at all | Fastest | Pure logic tests, form validation, utility functions |
TestCase | Transaction + rollback per method | Fast | The majority of tests — the first-choice default |
TransactionTestCase | Truncates tables after each method (real commits) | Slow | Testing on_commit(), multi-thread, raw SQL transactions |
LiveServerTestCase | Same as TransactionTestCase + a live HTTP server | Slowest | Selenium, end-to-end browser tests |
TestCase is enough for almost all cases because rollback is far faster than truncating tables. But there’s one classic trap: if your production code uses transaction.on_commit() to schedule something (for example sending an email after a successful order), that callback will never be called inside TestCase because the transaction is indeed never truly committed.
# ANTI-PATTERN: testing an on_commit() callback with a regular TestCase
from django.test import TestCase
from django.db import transaction
class OrderTest(TestCase):
def test_send_email_after_order(self):
with transaction.atomic():
order = Order.objects.create(status="paid")
transaction.on_commit(lambda: send_confirmation_email(order))
# the on_commit callback will NEVER run here,
# because TestCase's outer transaction is never committed
# CORRECT: use TransactionTestCase so a real commit happens
from django.test import TransactionTestCase
class OrderTest(TransactionTestCase):
def test_send_email_after_order(self):
with transaction.atomic():
order = Order.objects.create(status="paid")
transaction.on_commit(lambda: send_confirmation_email(order))
# here a real commit happens, the on_commit callback is called
Start withTestCaseas the default. Move up toTransactionTestCaseonly if your test needs real commit behavior — not the other way around, becauseTransactionTestCaseis far slower due to table truncation in every method.
Speeding Up Tests with --keepdb
Creating and deleting the test database on every run feels light for small projects, but once the number of migrations swells, this create-and-migrate step can take significant time — sometimes longer than the test execution itself. Django provides an official flag for this problem.
python manage.py test --keepdb
With this flag:
- Django checks whether the
test_<db_name>test database already exists - If it does, Django uses it directly without recreating it
- Migrations already run aren’t repeated — Django only runs new migrations not yet recorded
- The database isn’t deleted after tests finish, so subsequent runs get faster
flowchart TD
A[python manage.py test --keepdb] --> B{Test database already exists?}
B -- Yes --> C[Use the existing database]
B -- No --> D[Create a new database]
C --> E[Run unrecorded migrations]
D --> E
E --> F[Execute tests]
F --> G[Database NOT deleted]--keepdb is safe to use daily because the database retained is still a test database — not a production database. The only thing to remember: if you change migrations destructively (for example squashing migrations or editing old migrations), the kept test database can become out of sync. The solution is simply deleting that test database once manually, then rerunning without --keepdb to recreate it fresh.
Parallel Testing to Speed Up Large Suites
Besides --keepdb, Django also has a built-in mechanism for running tests in parallel across many processes at once — very useful when a test suite already contains thousands of tests and runs on CI with many CPU cores.
python manage.py test --parallel=4
Each worker gets its own test database, usually with a numeric suffix: test_mydb_1, test_mydb_2, test_mydb_3, and so on. Django automatically distributes test classes to these workers.
flowchart TD
A[manage.py test --parallel=4] --> B[Worker 1: test_mydb_1]
A --> C[Worker 2: test_mydb_2]
A --> D[Worker 3: test_mydb_3]
A --> E[Worker 4: test_mydb_4]
B --> F[Results combined]
C --> F
D --> F
E --> F--parallel can be combined with --keepdb for the fastest results:
python manage.py test --parallel=4 --keepdb
Parallel testing assumes your tests are independent of each other. Tests depending on execution order (for example test B assumes data from test A still exists) will fail inconsistently when run in parallel, because both could end up on different workers and databases.
What “Existing Database” Means
After understanding the default mechanism, it makes sense to discuss the term “existing database” — because this term actually refers to two very different scenarios that are often confused.
A test database that was previously created — this was actually covered above: a test database left over from --keepdb still on the server. Reusing it is completely safe because it remains within Django’s official test cycle.
An external database containing real data — staging or production, already used by a real application and containing real user data. This is what most people mean when asking “how do I test using an existing database”, and this is the risky one.
These two scenarios need very different approaches and levels of caution, so it’s important to make sure you know which one you’re talking about before deciding how to proceed.
Using an External Database for Tests
Django, by design, doesn’t directly support using an external database as a test target. There’s no official setting like TEST_USE_EXISTING_DB = True. But in certain cases — integration tests against legacy systems, or read-only verification of real data — some teams choose to manually override the database configuration using pytest-django.
This override is done in conftest.py, by replacing the django_db_setup fixture so Django connects directly to the target database instead of creating a new test database:
# conftest.py
import pytest
from django.conf import settings
@pytest.fixture(scope="session")
def django_db_setup():
settings.DATABASES['default'] = {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'existing_db',
'USER': 'readonly_user',
'PASSWORD': 'secret',
'HOST': 'db.example.com',
'PORT': '5432',
}
# Note: with this fixture overridden,
# pytest-django will not create/delete a test database
Once this fixture is active, the entire test session connects directly to existing_db — not to a temporary test database. Note the use of readonly_user in the credentials; this isn’t a coincidence, but the minimum mitigation that must exist if you choose this approach.
This override approach only makes sense for read-only tests against an external database. Never run tests that docreate(),update(),delete(), or migrations against a database overridden this way — the risk is permanently losing or corrupting real data.
Risks of Using an Existing Database for Tests
Running tests directly on top of an external database has several structural risks that can’t be avoided, not just a matter of individual developer caution:
- Data can change or be deleted — tests calling
.save()or.delete()directly affect real data, without the automatic rollback ofTestCase - Tests depend on data state — test results can differ depending on what data happens to exist in the database at that moment, making tests non-reproducible
- No isolation between tests — one test can “poison” the state for other tests running after it
- Hard to roll back — there’s no built-in mechanism to restore the database to its original state after a test fails midway
- Migrations could run unintentionally — if Django detects pending migrations, it could try running them against a database currently used by a production application
For these reasons Django chose from the start not to provide an official path to this approach. This isn’t a framework limitation, but a design decision protecting developers from hard-to-undo mistakes.
Safer Alternatives
If your motivation for using an existing database is wanting realistic data (not just speeding up tests, which --keepdb already answers), there are several approaches giving similar results without the risk of touching real data.
Fixtures from Existing Data
Take a data snapshot from the existing database, save it as a fixture, then load that fixture into the test database whenever needed.
# Take data from the existing database, save it as a fixture
python manage.py dumpdata app.Product --indent=2 > fixtures/product.json
# Load the fixture when the test runs — automatically goes into the isolated test database
from django.test import TestCase
class ProductTest(TestCase):
fixtures = ['product.json']
def test_product_count_matches_fixture(self):
self.assertTrue(Product.objects.exists())
This way, tests still run in a fully isolated test database — the fixture only injects realistic initial data, and TestCase rollback still applies normally afterward.
Factory Pattern with factory_boy
For data that needs to vary between tests (not just a static dump), the factory pattern is more flexible than rigid JSON fixtures:
import factory
from myapp.models import Product
class ProductFactory(factory.django.DjangoModelFactory):
class Meta:
model = Product
name = factory.Sequence(lambda n: f"Product {n}")
price = factory.Faker('random_int', min=10000, max=500000)
# Usage in a test
class ProductTest(TestCase):
def test_product_has_positive_price(self):
product = ProductFactory()
self.assertGreater(product.price, 0)
Factories generate new data every time they’re called, still within the isolated test database, and are far easier to adjust per test scenario than static fixtures.
Mocking for Pure Unit Tests
For logic that doesn’t actually need a real database — validation, calculations, data transformations — mocking avoids database dependency entirely, both test and production.
from unittest.mock import patch
from django.test import SimpleTestCase
class CalculateDiscountTest(SimpleTestCase):
@patch('myapp.services.Product.objects.get')
def test_calculate_discount(self, mock_get):
mock_get.return_value.price = 100000
result = calculate_discount(product_id=1, percent=10)
self.assertEqual(result, 90000)
SimpleTestCase even forbids database access by default — if this test accidentally calls the ORM without mocking, Django raises an explicit error instead of silently connecting to a database.
Custom Test Runner
For very specific needs — for example changing the database setup order or adding a verification step before tests run — you can create your own test runner by overriding setup_databases and teardown_databases from DiscoverRunner. This approach is rarely truly necessary and adds maintenance complexity, so consider first whether --keepdb, fixtures, or factories are enough before going down this path.
Strategy Summary Table
| Need | Recommended Approach |
|---|---|
| Standard daily tests | TestCase + Django’s built-in test database |
| Speeding up repeated runs | --keepdb |
| Large test suites on CI | --parallel combined with --keepdb |
Testing on_commit() or multi-thread | TransactionTestCase |
| Realistic test data | Fixtures (dumpdata / loaddata) |
| Varied data between tests | Factory pattern (factory_boy) |
| Pure logic without DB | SimpleTestCase + mocking |
| Integration tests against a legacy system (read-only) | Override pytest-django with a readonly_user |
| Direct access to a production database | ❌ Not recommended under any circumstances |
Summary
- Django always creates a separate test database by default — a create, migrate, run, destroy process — so tests are reproducible and safe from production data.
TestCasewraps each test method in a rolled-back transaction, giving automatic isolation without manual cleanup.- Use
TransactionTestCaseonly when real commits are needed, for example to testtransaction.on_commit()— this base class is far slower because it truncates tables in every method.--keepdbis the official and safe way to speed up repeated tests by retaining the test database between runs.--parallelspeeds up large suites by distributing tests across many workers, each with its own test database — but assumes your tests are independent of each other.- “Existing database” has two meanings: an old test database (safe to reuse) vs an external database with real data (high risk).
- Overriding an external database via
pytest-djangoonly makes sense for read-only tests, with read-only credentials as the minimum mitigation.- Fixtures and factory_boy provide realistic data without touching a real database, while still enjoying the full isolation of
TestCase.- Avoid running any tests directly on top of a production database — the risk of losing real data far outweighs any speed benefit gained.