gmoss
gmoss

Reputation: 989

getting a mock to run before a pytest fixture is executed

I'm trying to conditionally mock something in a function-scoped fixture differently for different tests, so I don't have to create two entirely separate fixtures with nearly-identical code.

MY_GLOB_VAR = False

def mocked_build_data():
    with conditional(MY_GLOB_VAR, mock.patch(stuff)):
        build_data()

class Tests:
    @pytest.fixture(scope='function')
    def my_fixture(self, conftest_fixtures):
        inject_test_specific_data_into_db(conftest_fixtures)
        mocked_build_data()
        yield
        cleanup_mysql()

    def test_1(self, my_fixture):
        assert True

    @mock.patch(f'{__name__}.MY_GLOB_VAR', True)
    def test_2(self, my_fixture):
         assert True

I need the mock on test_2 to be applied before my_fixture is executed, but it's only being applied once I'm in the function body.

Can I either force this the way I'm doing it, or conditionally mock the fixture some other way?

Upvotes: 3

Views: 2195

Answers (2)

Romain
Romain

Reputation: 21878

You can use the indirect parametrization feature of Pytest for that.

Using the indirect=True parameter when parametrizing a test allows to parametrize a test with a fixture receiving the values before passing them to a test.

In your case you could mock the object in the fixture according to the value it receives (through the request param). Here is an example showing the principle.

import pytest

MY_GLOB_VAR = None


@pytest.fixture(scope="function")
def my_fixture(request):
    if hasattr(request, "param"):
        # use it without param (default case)
        global MY_GLOB_VAR
        MY_GLOB_VAR = request.param
    yield
    # restore initial status
    MY_GLOB_VAR = None


@pytest.mark.parametrize("my_fixture", [True], indirect=True)
def test_true(my_fixture):
    assert MY_GLOB_VAR == True


@pytest.mark.parametrize("my_fixture", [False], indirect=True)
def test_false(my_fixture):
    assert MY_GLOB_VAR == False


def test_none(my_fixture):
    """Example without parametrization"""
    assert MY_GLOB_VAR is None

Run the tests

pytest test_mock.py
# test_mock.py::test_true[True] PASSED
# test_mock.py::test_false[False] PASSED
# test_mock.py::test_none PASSED                                                                                                                                              ```

Upvotes: 1

hoefling
hoefling

Reputation: 66181

You can also move patching in a separate fixture and make sure it is requested before my_fixture in test_2. Fixtures are executed in the order they are listed in test function arguments, so in the signature def test(fix1, fix2): the fixture fix1 will be executed before fix2 etc. Specifically for your example:

MY_GLOB_VAR = False

class Tests:
    @pytest.fixture
    def patch_my_glob_var(self):
        with mock.patch(f'{__name__}.MY_GLOB_VAR', True):
            yield

    @pytest.fixture(scope='function')
    def my_fixture(self):
        print('MY_GLOB_VAR is', MY_GLOB_VAR)
        yield

    def test_1(self, my_fixture):
        assert MY_GLOB_VAR is False

    def test_2(self, patch_my_glob_var, my_fixture):
        assert MY_GLOB_VAR is True

Running it with the -s flag should yield

some_test_module::Tests::test_1 MY_GLOB_VAR is False
PASSED
some_test_module::Tests::test_2 MY_GLOB_VAR is True
PASSED

Another option would be requesting the fixture via the usefixtures marker, since its returned result isn't explicitly used in the test. This will lead to the same result though, as patch_my_glob_var will still be evaluated before my_fixture:

@pytest.mark.usefixtures('patch_my_glob_var')
def test_2(self, my_fixture):
    ...

Upvotes: 3

Related Questions