Search code examples
pythondjangopostgresqlpytestpytest-django

install postgresql extension before pytest set up database for django


I need to install citext extension to my postgresql database for django project. For the project itself it went smoothly and works great via migrations, but my pytest is configured with option --no-migrations, so pytest create database without running migrations. How can i make pytest to install citext postgres extension before tables are created? Currently i'm getting

- django.db.utils.ProgrammingError: type "citext" does not exist

while pytest trying to create table auth_users

sql = 'CREATE TABLE "auth_user" ("id" serial NOT NULL PRIMARY KEY, "password" varchar(128) NOT NULL, "last_login" timestamp ...T NULL, "is_active" boolean NOT NULL, "date_joined" timestamp with time zone NOT NULL, "email" citext NOT NULL UNIQUE)', params = None
ignored_wrapper_args = (False, {'connection': <django.contrib.gis.db.backends.postgis.base.DatabaseWrapper object at 0x7fb313bb0100>, 'cursor': <django.db.backends.utils.CursorWrapper object at 0x7fb30d9f8580>})

I tried to use django_db_setup fixture, but i did not figure out how to change it, because something like this

@pytest.fixture(scope="session")
def django_db_setup(
    request,
    django_test_environment,
    django_db_blocker,
    django_db_use_migrations,
    django_db_keepdb,
    django_db_createdb,
    django_db_modify_db_settings,
):
    """Top level fixture to ensure test databases are available"""
    from django.test.utils import setup_databases, teardown_databases

    setup_databases_args = {}

    if not django_db_use_migrations:
        from pytest_django.fixtures import _disable_native_migrations
        _disable_native_migrations()

    if django_db_keepdb and not django_db_createdb:
        setup_databases_args["keepdb"] = True

    with django_db_blocker.unblock():
        from django.db import connection
        cursor = connection.cursor()
        cursor.execute("CREATE EXTENSION IF NOT EXISTS citext;")

        db_cfg = setup_databases(
            verbosity=request.config.option.verbose,
            interactive=False,
            **setup_databases_args
        )
    def teardown_database():
        with django_db_blocker.unblock():
            try:
                teardown_databases(db_cfg, verbosity=request.config.option.verbose)
            except Exception as exc:
                request.node.warn(
                    pytest.PytestWarning(
                        "Error when trying to teardown test databases: %r" % exc
                    )
                )

    if not django_db_keepdb:
        request.addfinalizer(teardown_database)

did not help me


Solution

  • Annoyingly, there are no appropriate hooks between setting up the database and loading the appropriate postgresql extensions. You can work around the issue by copying/modifying the pytest-django code that disables migrations and running your code instead of the upstream code.

    @pytest.fixture(scope="session")
    def django_migration_disabler() -> None:
        """Disable migrations when running django tests.
    
        This copies/alters the behavior of pytest_django.fixtures._disable_migrations,
        which is called when pytest is invoked with --no-migrations.
    
        See: https://github.com/pytest-dev/pytest-django/blob/v4.5.2/pytest_django/fixtures.py#L260
    
        We do this instead of invoking with --no-migrations because constructing the
        database without migrations fails to create necessary postgres extensions like
        citext and uuid-ossp.
    
        We then override the django_db_setup fixture and ensure that this
        fixture is called before the parent django_db_setup fixture.
        """
        from django.conf import settings
        from django.core.management.commands import migrate
        from django.db import connections
    
        class DisableMigrations:
            def __contains__(self, item: str) -> bool:
                return True
    
            def __getitem__(self, item: str) -> None:
                return None
    
        settings.MIGRATION_MODULES = DisableMigrations()
    
        class MigrateSilentCommand(migrate.Command):
            def handle(self, *args, **options):
                options["verbosity"] = 0
                database = options["database"]
                connection = connections[database]
                with connection.cursor() as cursor:
                    cursor.execute('CREATE EXTENSION IF NOT EXISTS "citext";')
                return super().handle(*args, **options)
    
        migrate.Command = MigrateSilentCommand  # type: ignore
    
    
    @pytest.fixture(scope="session")
    def django_db_use_migrations() -> bool:
        """Force pytest-django to use migrations.
    
        This is necessary because we disable migrations in the django_migration_disabler
        and the existing pytest-django mechanisms would override our disabling mechanism
        with their own, which would fail to create the necessary extensions in postgres.
        """
        return True
    
    
    @pytest.fixture(scope="session")
    def django_db_setup(django_migration_disabler, django_db_setup, django_db_blocker):
        """Override django_db_setup ensuring django_migration_disabler is loaded."""
        pass