Search code examples
pythondependency-injectionpytestfixtures

Add PyTest fixtures to a test class using dependency injection


I'm using Python 3 with PyTest and have defined a number of fixture objects in conftest.py. The problem I'm having is that there are some fixture objects that will be needed by every test case. Requesting these fixture references in all test cases results in a lot of repeated boilerplate code.

Here's the fixture in conftest.py:

def fixtureA(request):
    _fixture = FixtureA()
    # initialize the fixture, do stuff with request
    return _fixture

Here's the current test class, where I want to remove all fixtures from arguments for readability:

class TestSomeFeature(BaseTest):

    def test_some_function(self, fixtureA, fixtureB, fixtureC):
        fixtureA.doSomething()
        # execute test case, do stuff with other fixtures     

    def test_some_other_function(self, fixtureA, fixtureB, fixtureC):
        data = fixtureB.getData()
        # execute test case

This approach works, but I'd like to find a way to use dependency injection (or similar) to automagically inject the fixtures in BaseTest attributes without having to specify them in every test case's argument list. I'm looking for something like this, but open to any other suggestions:

class BaseTest:
    # This does not work, since pytest does not allow constructors in the test class
    def __init__(fixtureA, fixtureB, fixtureC):
        self.fixtureA = fixtureA
        self.fixtureB = fixtureB
        self.fixtureC = fixtureC

I want the test class to look like this, much cleaner!

class TestSomeFeature(BaseTest):

    def test_some_function(self):
        self.FixtureA.doSomething()
        # execute test case

    def test_some_other_function(self):
        data = self.FixtureB.getData()
        # execute test case

Solution

  • First, you can define fixtures both in conftest.py and in test classes. The difference is visibility: if you define a fixture in a conftest.py, it is visible to all tests on the level of that conftest.py file and below. If you define it inside a test module, it is visible in this module only. If you define it inside a test class, it is visible in this class and derived classes.

    Also note that you can use autotest=True also if you return a value - you just have to reference the fixture in the respective tests. You can also save the fixture value in a variable. Here is a simplistic example for both cases if you are are using a base class:

    class TestBase:
        @pytest.fixture(autouse=True)
        def fixture1(self):
            self.value1 = 1  # save the fixture value
            yield
    
        @pytest.fixture
        def fixture2(self):
            yield 2  # return the fixture value - fixtue has to be referenced
            # autouse can still make sense if there is setup/tearDown code,
            # and the fixture must not be referenced in all of the tests
    
        @pytest.fixture(autouse=True)
        def fixture3(self):
            self.value3 = 3
            yield 3  # do both - can be used either way
    
    
    class TestDerived(TestBase):
        def test_1(self):
            assert self.value1 == 1
    
        def test_2(self, fixture2):
            assert fixture2 == 2
    
        def test_3_1(self):
            assert self.value3 == 3
    
        def test_3_2(self, fixture3):
            assert fixture3 == 3
    

    Note that you get the fixture value, not the fixture itself, if you refer to the fixture, so there is no need (and it is not possible) to call the fixture - instead you directly use the value returned by the fixture.