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 scope fixture not working with indirect parametrization since 5.4.0 #9198

Open palocziarpad opened 2 years ago

palocziarpad commented 2 years ago

Pytest - 5.4.0

pytest_version_test/
├── conftest.py
├── __init__.py
└── test_a.py

conftest.py

import pytest

def pytest_addoption(parser):
    parser.addoption("--indirect_params", action="store", default=None)

def pytest_generate_tests(metafunc):
    if 'indirect_param' in metafunc.fixturenames:
        alarms = metafunc.config.getoption("--indirect_params").split(',')
        metafunc.parametrize("indirect_param", alarms, indirect=True, scope='session')

@pytest.fixture(scope='session')
def indirect_param(request):
    return request.param

@pytest.fixture(scope='session', autouse=True)
def ses_fix():
    print('session fixture start')
    yield
print('session fixture end')

@pytest.fixture(scope='package', autouse=True)
def pak_fix():
    print('package fixture start')
    yield
print('package fixture end')

@pytest.fixture(scope='module', autouse=True)
def mod_fix():
    print('module fixture start')
    yield
print('module fixture end')

@pytest.fixture(scope='function', autouse=True)
def func_fix():
    print('function fixture start')
    yield
print('function fixture end')

test_a.py

import pytest

@pytest.fixture(scope='class')
def cls_fix(indirect_param):
    print(f'class fixture start with param {​​​​​​​​​indirect_param}​​​​​​​​​')
    yield
print(f'class fixture ends with param {​​​​​​​​​indirect_param}​​​​​​​​​')

@pytest.fixture(scope='class')
def another_cls_fix():
    print(f'Another class fixture start')
    yield
print(f'Another class fixture ends ')

class TestA:
    def test_a1(self, cls_fix):
        assert True
    def test_a2(self, cls_fix):
        assert True
    def test_a3(self, cls_fix):
        assert True
class TestB:
    def test_b1(self, another_cls_fix):
        assert True
    def test_b2(self, another_cls_fix):
        assert True
    def test_b3(self, another_cls_fix):
        assert True

Incorrect behavior

Expectation is that the "class fixture start with param ABC" written out only once.

*****Output from TestA****************
session fixture start
package fixture start
module fixture start
class fixture start with param ABC
function fixture start
PASSED                                    [ 11%]function fixture end
class fixture ends with param ABC
class fixture start with param ABC
function fixture start
PASSED                                    [ 22%]function fixture end
class fixture ends with param ABC
class fixture start with param ABC
function fixture start
PASSED                                    [ 33%]function fixture end
class fixture ends with param ABC
class fixture start with param DEF
function fixture start
PASSED                                    [ 44%]function fixture end
class fixture ends with param DEF
class fixture start with param DEF
function fixture start
PASSED                                    [ 55%]function fixture end

test_a.py::TestA::test_a3[DEF] class fixture ends with param DEF
class fixture start with param DEF
function fixture start
PASSED                                    [ 66%]function fixture end
class fixture ends with param DEF

Without the indirect parametization: The indirect parametization version should work in a similar way.


*****Output from TestB****************

Another class fixture start
function fixture start
PASSED                                         [ 77%]function fixture end
function fixture start
PASSED                                         [ 88%]function fixture end
function fixture start
PASSED                                         [100%]function fixture end
Another class fixture ends 
module fixture end
package fixture end
session fixture end
holmuk commented 2 years ago

I had the same issue. But I doubt it should be considered a Pytest bug.

The commit 80d4dd6f0b... changed the old way of comparing cache keys' values with == in favor of identity comparison.

During the collection stage pytest_generate_tests is called for each function TestA.test_a, TestB.test_b, etc, and each time a new metafunc is generated. It means that metafunc.parametrize is called more than once for alarms where the elements' addresses/identities are not the same between invocations (as they're produced with split). Note that metafunc.config.getoption("--indirect-params") is always the same object.

As a quick solution, you can get rid of split and use --indirect-params just to accumulate values:

def pytest_addoption(parser):
    parser.addoption("--indirect-params", action="append")

def pytest_generate_tests(metafunc):
    if 'indirect_param' in metafunc.fixturenames:
        alarms = metafunc.config.getoption("--indirect-params")
        metafunc.parametrize("indirect_param", alarms, indirect=True)