Is it possible to pass yielding pytest fixtures (for setup and teardown) as parameters to test functions?
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:
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.
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
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)
To restate the question: Is there anyway to pass these fixtures as parameters and maintain the cleanup of the temporary directory?
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)