2025, Oct 04 13:00

Avoid Django connection_created pitfalls in tests: use post_migrate and AppConfig.ready to sync notification types

Resolve Django PostgreSQL pytest warnings from connection_created by syncing notification types after migrations with post_migrate and AppConfig.ready

Keeping a canonical list of notification types in sync between code and database looks deceptively simple until tests enter the picture. A straightforward connection-level hook appears to do the job during local runs, yet it triggers noisy warnings under pytest with PostgreSQL. Below is a practical walkthrough of what goes wrong and how to make it reliable across runserver and test environments.

The setup: syncing notification types on DB connection

The goal is to store a registry of supported notification types in the database, maintain their metadata, and keep a many-to-many mapping with customers. The logic for each notification lives in its own class, and a singleton-like container aggregates their metadata. The first attempt uses the connection_created signal to synchronize database rows as soon as a connection is opened.

from django.db.backends.signals import connection_created
from django.db.backends.sqlite3.base import DatabaseWrapper as SqliteWrapper
from django.db.backends.postgresql.base import DatabaseWrapper as PgWrapper
from django.dispatch import receiver

from notify_catalog import NotifyCatalog
from models import NoticeType


@receiver(connection_created, sender=SqliteWrapper)
@receiver(connection_created, sender=PgWrapper)
def sync_notice_kinds(sender, **kwargs):
    active_ids = []
    for _, kind in NotifyCatalog._data.items():
        obj, _ = NoticeType.objects.update_or_create(
            name=kind.name,
            defaults={
                'description': kind.description,
                'is_active': True,
            },
        )
        active_ids.append(obj.id)

    # Deactivate all notifications in the database that are not used
    NoticeType.objects.exclude(id__in=active_ids).update(is_active=False)

    # Update the in-memory registry with the active events
    NotifyCatalog._registry = {
        item.name: item
        for item in NoticeType.objects.filter(is_active=True)
    }

What actually happens and why tests complain

The connection_created signal fires very early in Django’s database lifecycle. In tests, Django first talks to the PostgreSQL admin database to prepare test databases. If application-level ORM writes are issued at that moment, they land before the app’s database is fully ready for normal operations. That timing mismatch is exactly what surfaces as the RuntimeWarning about Django falling back to the first PostgreSQL database during test setup. In other words, the hook is legitimate for low-level connection tweaks, but it becomes brittle when used to seed or mutate application tables while the testing infrastructure is still bootstrapping.

A stable fix: postpone seeding to post_migrate, and cover runserver via AppConfig.ready

Deferring synchronization to a later lifecycle phase removes the race with test database creation. The post_migrate signal runs after migrations are applied, which is the right moment to upsert metadata rows. For normal server startups, you can run the same initialization from AppConfig.ready, guarded by a table existence check so it doesn’t break on first-run scenarios.

from django.db.models.signals import post_migrate
from django.dispatch import receiver
from notify_catalog import NotifyCatalog
from models import NoticeType

@receiver(post_migrate)
def ensure_notice_kinds(sender, **kwargs):
    # Only run for the intended app
    if sender.name != 'your_app_name':
        return

    current_ids = []
    for _, kind in NotifyCatalog._data.items():
        obj, _ = NoticeType.objects.update_or_create(
            name=kind.name,
            defaults={
                'description': kind.description,
                'is_active': True,
            },
        )
        current_ids.append(obj.id)

    # Deactivate unused notifications
    NoticeType.objects.exclude(id__in=current_ids).update(is_active=False)

    # Refresh the in-memory registry
    NotifyCatalog._registry = {
        item.name: item
        for item in NoticeType.objects.filter(is_active=True)
    }

Because post_migrate is tied to executing migrations, it won’t automatically run on a plain runserver if no migrations are applied during that start. To make initialization consistent, wire the same logic from your AppConfig.ready method, while also registering the post_migrate handler.

# apps.py
from django.apps import AppConfig
from django.db.models.signals import post_migrate

class AlertsAppConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'your_app_name'

    def ready(self):
        # Run initialization on server start if tables exist
        if self._has_tables():
            self._seed_notice_kinds()

        # Ensure it also runs after migrations
        post_migrate.connect(self._after_migrate, sender=self)

    def _has_tables(self):
        from django.db import connection
        names = connection.introspection.table_names()
        return 'your_app_notificationtype' in names

    def _seed_notice_kinds(self):
        from .models import NoticeType
        from .notify_catalog import NotifyCatalog

        present_ids = []
        for _, kind in NotifyCatalog._data.items():
            obj, _ = NoticeType.objects.update_or_create(
                name=kind.name,
                defaults={
                    'description': kind.description,
                    'is_active': True,
                },
            )
            present_ids.append(obj.id)

        NoticeType.objects.exclude(id__in=present_ids).update(is_active=False)

        NotifyCatalog._registry = {
            item.name: item
            for item in NoticeType.objects.filter(is_active=True)
        }

    def _after_migrate(self, sender, **kwargs):
        if sender.name == self.name:
            self._seed_notice_kinds()

Why this matters

Signals tied to database connections execute at moments when the testing harness is still negotiating which database to touch. Running application-level writes in that limbo creates hard-to-diagnose side effects and undermines test reliability. Moving the synchronization to post_migrate aligns data seeding with schema readiness, and covering runserver via AppConfig.ready keeps behavior consistent in development without relying on connection timing.

Wrap-up

Use connection_created for database-session tweaks, not for populating or mutating application tables. Seed or sync notification types after migrations with post_migrate, and call the same routine from AppConfig.ready when tables already exist. This keeps your registry authoritative, prevents warnings under pytest with PostgreSQL, and ensures subscriptions map cleanly to the correct set of active notification types in every environment.

The article is based on a question from StackOverflow by Charalamm and an answer by Mahrez BenHamad.