pytest-dev / pytest-django

A Django plugin for pytest.
https://pytest-django.readthedocs.io/
Other
1.4k stars 345 forks source link

Unable to fully override db setup fixture #1131

Open eldjh3 opened 4 months ago

eldjh3 commented 4 months ago

My project uses django_audit_log. When pytest creates the test database it fails because whilst creating it and applying migrations it generates models. These trigger the auditlog, whose own latest migrations have not yet been applied, which attempts to insert an audit record. This fails due to a database schema mismatch with the audit log model.

To avoid the issue I would like to override the django_db_setup fixture, something akin to the following:

@pytest.fixture(scope="session")
def django_db_setup(django_db_setup):
    from auditlog.context import disable_auditlog
    with disable_auditlog():
        # Invoke the db_setup logic here...

The issue of course is that by the time the overriding fixture is called, the overridden one has already been executed. The examples at https://docs.pytest.org/en/7.1.x/how-to/fixtures.html#overriding-fixtures-on-various-levels show various means of overriding fixtures but they are all fairly trivial, there are no examples where the fixture does something relatively heavyweight.

I don't want to copy/paste the logic of django_db_setup and amend as that will likely lead to issues if the code changes in future.

To ease this, would you consider splitting "complex" fixtures such as this into two parts? The fixture and an implementing function, the fixture simply delegating to the function? The fixture should also be documented as simply delegating to the defined function.

This would allow users to override just the fixture, but reuse the implementation.

The implementation itself can't be a fixture as the same issue arises. If it's passed as a dependency then it's already executed by the time the overriding fixture is invoked.

One alternative is to have a delegate fixture that returns a function that invokes the original fixture, wrapping with whatever logic is desired. The issue here is that pytest checks for direct calls and rejects them. This of course can be worked around by poking inside the wrapped object, but that too is a little 'ick'.

E.g.

@pytest.fixture(scope="session")
def django_db_setup(db_setup_delegate):
    db_setup_delegate()

@pytest.fixture(scope="session")
def db_setup_delegate(request, django_test_environment,    \
    django_db_blocker, django_db_use_migrations,           \
    django_db_keepdb, django_db_createdb, django_db_modify_db_settings):

    def do_setup():
        from auditlog.context import disable_auditlog
        with disable_auditlog():
            from pytest_django.fixtures import django_db_setup as dbs
            # This fails if called on dbs directly
            next(dbs.__pytest_wrapped__.obj(
                request,
                django_test_environment,
                django_db_blocker,
                django_db_use_migrations,
                django_db_keepdb,
                django_db_createdb,
                django_db_modify_db_settings)
            )

    return do_setup

Thoughts? Is there an easy solution I've missed?

Thanks