pythonpytest

Passing (yield) fixtures as test parameters (with a temp directory)


Question

Is it possible to pass yielding pytest fixtures (for setup and teardown) as parameters to test functions?

Context

I'm testing an object that reads and writes data from/to files in a single directory. That path of that directory is saved as an attribute of the object.

I'm having trouble with the following:

  1. using a temporary directory with my test; and
  2. ensuring that the directory is removed after each test.

Example

Consider the following (test_yieldfixtures.py):

import pytest, tempfile, os, shutil
from contextlib import contextmanager
    
@contextmanager
def data():
    datadir = tempfile.mkdtemp()  # setup
    yield datadir
    shutil.rmtree(datadir)        # teardown
    
class Thing:
    def __init__(self, datadir, errorfile):
        self.datadir = datadir
        self.errorfile = errorfile

    
@pytest.fixture
def thing1():
    with data() as datadir:
        errorfile = os.path.join(datadir, 'testlog1.log')
        yield Thing(datadir=datadir, errorfile=errorfile)

@pytest.fixture
def thing2():
    with data() as datadir:
        errorfile = os.path.join(datadir, 'testlog2.log')
        yield Thing(datadir=datadir, errorfile=errorfile)

@pytest.mark.parametrize('thing', [thing1, thing2])
def test_attr(thing):
    print(thing.datadir)
    assert os.path.exists(thing.datadir)

Running pytest test_yieldfixtures.py outputs the following:

================================== FAILURES ===================================
______________________________ test_attr[thing0] ______________________________

thing = <generator object thing1 at 0x0000017B50C61BF8>

    @pytest.mark.parametrize('thing', [thing1, thing2])
    def test_attr(thing):
>        print(thing.datadir)
E       AttributeError: 'function' object has no attribute 'props'

test_mod.py:39: AttributeError

OK. So fixture functions don't have a the properties of my class. Fair enough.

Attempt 1

A function won't have the properties, so I tried calling that functions to actually get the objects. However, that just

@pytest.mark.parametrize('thing', [thing1(), thing2()])
def test_attr(thing):
    print(thing.props['datadir'])
    assert os.path.exists(thing.get('datadir'))

Results in:

================================== FAILURES ===================================
______________________________ test_attr[thing0] ______________________________

thing = <generator object thing1 at 0x0000017B50C61BF8>

    @pytest.mark.parametrize('thing', [thing1(), thing2()])
    def test_attr(thing):
>       print(thing.datadir)
E       AttributeError: 'generator' object has no attribute 'props'

test_mod.py:39: AttributeError

Attempt 2

I also tried using return instead of yield in the thing1/2 fixtures, but that kicks me out of the data context manager and removes the directory:

================================== FAILURES ===================================
______________________________ test_attr[thing0] ______________________________

thing = <test_mod.Thing object at 0x000001C528F05358>

    @pytest.mark.parametrize('thing', [thing1(), thing2()])
    def test_attr(thing):
        print(thing.datadir)
>       assert os.path.exists(thing.datadir)

Closing

To restate the question: Is there anyway to pass these fixtures as parameters and maintain the cleanup of the temporary directory?


Solution

  • Try making your data function / generator into a fixture. Then use request.getfixturevalue() to dynamically run the named fixture.

    import pytest, tempfile, os, shutil
    from contextlib import contextmanager
    
    @pytest.fixture # This works with pytest>3.0, on pytest<3.0 use yield_fixture
    def datadir():
        datadir = tempfile.mkdtemp()  # setup
        yield datadir
        shutil.rmtree(datadir)        # teardown
    
    class Thing:
        def __init__(self, datadir, errorfile):
            self.datadir = datadir
            self.errorfile = errorfile
    
    
    @pytest.fixture
    def thing1(datadir):
        errorfile = os.path.join(datadir, 'testlog1.log')
        yield Thing(datadir=datadir, errorfile=errorfile)
    
    @pytest.fixture
    def thing2(datadir):
        errorfile = os.path.join(datadir, 'testlog2.log')
        yield Thing(datadir=datadir, errorfile=errorfile)
    
    @pytest.mark.parametrize('thing_fixture_name', ['thing1', 'thing2'])
    def test_attr(request, thing):
        thing = request.getfixturevalue(thing) # This works with pytest>3.0, on pytest<3.0 use getfuncargvalue
        print(thing.datadir)
        assert os.path.exists(thing.datadir)
    

    Going one step futher, you can parametrize the thing fixtures like so:

    class Thing:
        def __init__(self, datadir, errorfile):
            self.datadir = datadir
            self.errorfile = errorfile
    
    @pytest.fixture(params=['test1.log', 'test2.log'])
    def thing(request):
        with tempfile.TemporaryDirectory() as datadir:
            errorfile = os.path.join(datadir, request.param)
            yield Thing(datadir=datadir, errorfile=errorfile)
    
    def test_thing_datadir(thing):
        assert os.path.exists(thing.datadir)