pytest-dev / pytest

The pytest framework makes it easy to write small tests, yet scales to support complex functional testing
https://pytest.org
MIT License
11.96k stars 2.66k forks source link

Class scoped fixture is called multiple times with mutable values and parameterisation #7175

Open JSchulte01 opened 4 years ago

JSchulte01 commented 4 years ago

When setting up test parameterisation using a combination of pytest_generate_test hooks and fixture decorators, along with pytest.mark.parameterize decorators I'm seeing inconsistent behaviour with the fixtures which have scope='class'. In the application context these fixtures load large tables of data which takes quite a long time.

When an immutable object is passed as the fixture parameter - the fixtures behave as expected, however when mutable object, like a list is passed, the class fixture gets called repeatedly. The workaround identified in #896 did not seem to make a difference so I have opened a new issue.

If this is expected behaviour or a known bug I would like to add a warning to the docs to make this clear.

Possibly Related

3678, #896

LSB Release

Distributor ID: Ubuntu
Description:    Ubuntu 16.04.6 LTS
Release:    16.04
Codename:   xenial

PIP Freeze >>

attrs==19.3.0
importlib-metadata==1.6.0
more-itertools==8.2.0
packaging==20.3
pluggy==0.13.1
py==1.8.1
pyparsing==2.4.7
pytest==5.4.1
six==1.14.0
wcwidth==0.1.9
zipp==3.1.0

Minimum Reproducible Example

import pytest

def pytest_generate_tests(metafunc):

    if 'fixture_a' in metafunc.fixturenames:
        metafunc.parametrize(
            'fixture_a',
            [1, ('a',), ['b']],
            scope='class',
            indirect=True,
        )

@pytest.fixture(scope='session')
def fixture_a(request):
    print('fixture_a CALLED')
    return request.param

class TestExp:

    @pytest.fixture(scope='class')
    def fixture_d(self, fixture_a):
        print('fixture_d CALLED')
        return fixture_a

    def test_func_one(self, fixture_d):
        print(fixture_d)

    @pytest.mark.parametrize(
        'param_a',
        ['*', '+']
    )
    def test_func_two(self, fixture_d, param_a):
        print(fixture_d, ' -> ', param_a)

Output of Test

platform linux -- Python 3.6.8, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 -- ...
cachedir: .pytest_cache
rootdir: ...
collected 9 items                                                                                                                                                                                          

test_mre.py::TestExp::test_func_one[1] 
fixture_a CALLED
fixture_d CALLED
1
PASSED
test_mre.py::TestExp::test_func_two[1-*] 1  ->  *
PASSED
test_mre.py::TestExp::test_func_two[1-+] 1  ->  +
PASSED

test_mre.py::TestExp::test_func_one[fixture_a1] 
fixture_a CALLED
fixture_d CALLED
('a',)
PASSED
test_mre.py::TestExp::test_func_two[fixture_a1-*] ('a',)  ->  *
PASSED
test_mre.py::TestExp::test_func_two[fixture_a1-+] ('a',)  ->  +
PASSED

test_mre.py::TestExp::test_func_one[fixture_a2] 
fixture_a CALLED
fixture_d CALLED
['b']
PASSED
test_mre.py::TestExp::test_func_two[fixture_a2-*]               <<< ---- Class scoped fixture called twice
fixture_a CALLED
fixture_d CALLED
['b']  ->  *
PASSED
test_mre.py::TestExp::test_func_two[fixture_a2-+] ['b']  ->  +
PASSED
symonk commented 4 years ago

smells like fixture caching / is vs == checks at a glance - will investigate