Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 460
zerohedge Avatar asked Aug 10 '18 09:08

zerohedge


People also ask

How do you use pytest fixtures?

To access the fixture function, the tests have to mention the fixture name as input parameter. Pytest while the test is getting executed, will see the fixture name as input parameter. It then executes the fixture function and the returned value is stored to the input parameter, which can be used by the test.

Can pytest fixtures use other fixtures?

A fixture can use multiple other fixtures. Just like a test method can take multiple fixtures as arguments, a fixture can take multiple other fixtures as arguments and use them to create the fixture value that it returns.

Where do I put pytest fixtures?

Fixtures and their visibility are a bit odd in pytest. They don't require importing, but if you defined them in a test_*. py file, they'll only be available in that file. You can however put them in a (project- or subfolder-wide) conftest.py to use them in multiple files.

Are pytest fixtures cached?

Pytest only caches one instance of a fixture at a time, which means that when using a parametrized fixture, pytest may invoke a fixture more than once in the given scope.


1 Answers

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
like image 126
hoefling Avatar answered Sep 17 '22 11:09

hoefling