pytest ScopeMismatch error: how to use fixtures properly
Asked Answered
B

1

38

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?

Bicentenary answered 10/8, 2018 at 9:50 Comment(0)
T
32

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
Tamis answered 10/8, 2018 at 11:7 Comment(1)
This was really helpful. On my end I also had to add django_db_setup for a pytest session fixture in order to have django_db_blocker in Django 4. Not sure exactly why that was the case, and may not be the case for others. But its something to try if you get an error that the table you're trying to access doesn't exist yetFerretti

© 2022 - 2024 — McMap. All rights reserved.