Pytest: mark.usefixtures registered fixtures of all test class subclasses invoked despite -k

Created on 29 Sep 2017  路  8Comments  路  Source: pytest-dev/pytest

If multiple test classes all subclass a class with shared tests and use the pytest.mark.usefixtures(), runs using -k SingleClass will still invoke excluded test class sourced fixtures.

reproduce.py

import pytest


@pytest.fixture
def one():
    print 'In one!'


@pytest.fixture
def two():
    print 'In two!'


@pytest.fixture
def three():
    print 'In three!'


class Base(object):

    def test_shared(self):
        assert 1


@pytest.mark.usefixtures('one')
class TestOne(Base):

    def test_one(self):
        assert 1


@pytest.mark.usefixtures('two')
class TestTwo(Base):

    def test_two(self):
        assert 1


@pytest.mark.usefixtures('three')
class TestThree(Base):

    def test_three(self):
        assert 1

OSX:tmp me$ pytest -vs reproduce.py -k Three
======================================================================== test session starts =========================================================================
platform darwin -- Python 2.7.13, pytest-3.2.2, py-1.4.34, pluggy-0.4.0 -- /Users/me/.virtualenvs/venv/bin/python2.7
cachedir: .cache
metadata: {'Python': '2.7.13', 'Platform': 'Darwin-15.5.0-x86_64-i386-64bit', 'Packages': {'py': '1.4.34', 'pytest': '3.2.2', 'pluggy': '0.4.0'}, 'Plugins': {'pylama': '7.4.1', 'cov': '2.5.1', 'ordering': '0.5', 'html': '1.15.2', 'catchlog': '1.2.2', 'instafail': '0.3.0', 'metadata': '1.5.0'}}
rootdir: /Users/me/tmp, inifile:
plugins: ordering-0.5, metadata-1.5.0, instafail-0.3.0, html-1.15.2, cov-2.5.1, catchlog-1.2.2, pylama-7.4.1

reproduce.py::TestThree::test_shared In one!
In two!
In three!
PASSED
reproduce.py::TestThree::test_three In three!
PASSED
fixtures marks selection bug

Most helpful comment

You can use a decorator to inject the autouse fixture for you:

def use_fixtures_workaround(*names):

    def inner(cls):

        @pytest.fixture(autouse=True)
        def __usefixtures_workaround(self, request):
            for name in names:
                request.getfixturevalue(name)
        cls.__usefixtures_workaround = __usefixtures_workaround
        return cls

    return inner

@use_fixtures_workaround('one')
class TestOne(Base):

    def test_one(self):
        assert 1

@use_fixtures_workaround('two')
class TestTwo(Base):

    def test_two(self):
        assert 1

@use_fixtures_workaround('three')
class TestThree(Base):

    def test_three(self):
        assert 1

All 8 comments

Hi @rmfitzpatrick,

You expected this output:

reproduce.py::TestThree::test_shared In three!
PASSED
reproduce.py::TestThree::test_three In three!
PASSED

Correct? (Btw, in the future try to add your expectation explicitly, it helps us maintainers sometimes :wink:)

This is a long term problem with our marks system, marks are attached to the python object that decorates them and accumulated there, instead of attached to the collection item the python object represents.

For example, when you decorate a test_foo function with a mark, the MarkInfo object is attached to the test_foo function. The same happens to classes, in which case this is more problematic because there's inheritance involved.

Fixtures, on the other hand, are attached to the collection item where they were declared. For example, a fixture declared in a module will be attached to the Module item created by pytest to represent that module, not to the python module object itself.

@RonnyPfannschmidt is bravely undergoing a crusade in order to fix this wart in our code-base.

For know there's no known solution to this problem, other than stop using usefixtures marker and use this ugly workaround instead:

class TestOne(Base):

    @pytest.fixture(autouse=True)
    def __foo(self, one):
        pass

class TestTwo(Base):
    @pytest.fixture(autouse=True)
    def __foo(self, two):
        pass

class TestThree(Base):
    @pytest.fixture(autouse=True)
    def __foo(self, three):
        pass

This forces your classes to instantiate the appropriate fixtures using the fixtures mechanism itself, and not marks. With this I can obtain the desired output:

collected 6 items

.tmp/test_usefixtures.py::TestThree::test_shared In three!
PASSED
.tmp/test_usefixtures.py::TestThree::test_three In three!
PASSED

============================= 4 tests deselected ==============================

Thanks @nicoddemus for the suggested workaround, ~though I'd rather not have to define fixtures for each class over using a single definition.~ (edit: the functionality of this just became clear to me)

You are correct that I'd only expect

.tmp/test_usefixtures.py::TestThree::test_shared In three!
PASSED
.tmp/test_usefixtures.py::TestThree::test_three In three!
PASSED

You can use a decorator to inject the autouse fixture for you:

def use_fixtures_workaround(*names):

    def inner(cls):

        @pytest.fixture(autouse=True)
        def __usefixtures_workaround(self, request):
            for name in names:
                request.getfixturevalue(name)
        cls.__usefixtures_workaround = __usefixtures_workaround
        return cls

    return inner

@use_fixtures_workaround('one')
class TestOne(Base):

    def test_one(self):
        assert 1

@use_fixtures_workaround('two')
class TestTwo(Base):

    def test_two(self):
        assert 1

@use_fixtures_workaround('three')
class TestThree(Base):

    def test_three(self):
        assert 1

Wow @nicoddemus ... that's some quite scary advanced pytest foo 馃

Hehehe!

At its heart, it is just a class decorator which injects a function, which happens to be decorated with @pytest.fixture(autouse=True) 馃槈

Einstein must have said something similar about his theory of general relativity 馃榿

535 and #568 are the culprit

Closed by #3317.

Was this page helpful?
0 / 5 - 0 ratings