Search code examples
pythonpytestpytest-django

pytest ScopeMismatch error: how to use fixtures properly


For the following piece of code:

@pytest.fixture(scope="module")
def dummy_article(request, db):
    return mixer.blend("core.article", title="this one price", internal_id=3000)


def test_article_str_method(dummy_article):
    assert (
        str(dummy_article)
        == f"article with ID {dummy_article.internal_id} and title: {dummy_article.title}"
    )

I'm getting the following error:

ScopeMismatch: You tried to access the 'function' scoped fixture 'db' with a 'module' scoped request object, involved factories
core/tests/test_article_model.py:13:  def dummy_article(request, db)

The error goes away if I change the fixture to use scope="function", but that defeats the purpose of having it available to other tests and not having to "set-up" for every test.

How can I have fixtures with db access that have a scope wider than function?


Solution

  • The db fixture has the function scope for a reason, so the transaction rollbacks on the end of each test ensure the database is left in the same state it has when test starts. Nevertheless, you can have the session/module scoped access to database in fixture by using the django_db_blocker fixture:

    @pytest.fixture(scope='module')
    def get_all_models(django_db_blocker):
        with django_db_blocker.unblock():
            return MyModel.objects.all()
    

    Warning

    Beware that when unlocking the database in session scope, you're on your own if you alter the database in other fixtures or tests. In the example below I create an entity of Foo in a session-scoped fixture create_foo, then cache the queryset for session in all_foos:

    # models.py
    
    from django.db import models
    
    class Foo(models.Model):
        name = models.CharField(max_length=16)
    


    # test_foo.py
    
    import pytest
    from app.models import Foo
    
    @pytest.fixture(scope='session', autouse=True)
    def create_foo(django_db_blocker):
        with django_db_blocker.unblock():
            Foo.objects.create(name='bar')
    
    
    @pytest.fixture(scope='module')
    def all_foos(django_db_blocker):
        with django_db_blocker.unblock():
            yield Foo.objects.all()
    
    
    def test_1(all_foos):
        assert all_foos.exists()
    
    def test_2(all_foos, db):
        all_foos.delete()
        assert not Foo.objects.exists()
    
    def test3(all_foos):
        assert all_foos.exists()
    

    After the test_2 runs, the queryset stored in session from all_foos will be empty, causing test_3 to fail:

    test_foo.py::test_1 PASSED                                                           [ 33%]
    test_foo.py::test_2 PASSED                                                           [ 66%]
    test_foo.py::test_3 FAILED                                                           [100%]
    
    ========================================= FAILURES ========================================
    __________________________________________ test_3 _________________________________________
    
    all_foos = <QuerySet []>
    
        def test_3(all_foos):
    >       assert all_foos.exists()
    E       assert False
    E        +  where False = <bound method QuerySet.exists of <QuerySet []>>()
    E        +    where <bound method QuerySet.exists of <QuerySet []>> = <QuerySet []>.exists
    
    test_foo.py:28: AssertionError
    

    Consequence: never store references in session scope if you don't want to introduce a global state that can change in tests. Query the data from database and return copies or serialized data, and so on.

    Example for a safe usage:

    @pytest.fixture(scope='session')
    def foo_names(django_db_blocker):
        with django_db_blocker.unblock():
            names = list(Foo.objects.values_list('name', flat=True))
        return names