Using an Existing Database in Django Tests
11 min read

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:

  1. Django reads the DATABASES['default'] configuration and creates a new database named test_<original_db_name>
  2. All migrations are run from scratch in this test database
  3. The test suite executes against a clean, fully migrated database
  4. 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 again

As 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_save signals 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 ClassIsolationSpeedWhen Used
SimpleTestCaseNo database access at allFastestPure logic tests, form validation, utility functions
TestCaseTransaction + rollback per methodFastThe majority of tests — the first-choice default
TransactionTestCaseTruncates tables after each method (real commits)SlowTesting on_commit(), multi-thread, raw SQL transactions
LiveServerTestCaseSame as TransactionTestCase + a live HTTP serverSlowestSelenium, 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 with TestCase as the default. Move up to TransactionTestCase only if your test needs real commit behavior — not the other way around, because TransactionTestCase is 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 do create(), 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 of TestCase
  • 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

NeedRecommended Approach
Standard daily testsTestCase + 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-threadTransactionTestCase
Realistic test dataFixtures (dumpdata / loaddata)
Varied data between testsFactory pattern (factory_boy)
Pure logic without DBSimpleTestCase + 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.
  • TestCase wraps each test method in a rolled-back transaction, giving automatic isolation without manual cleanup.
  • Use TransactionTestCase only when real commits are needed, for example to test transaction.on_commit() — this base class is far slower because it truncates tables in every method.
  • --keepdb is the official and safe way to speed up repeated tests by retaining the test database between runs.
  • --parallel speeds 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-django only 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.

Portfolio