Is it possible to pass yielding pytest fixtures (for setup and teardown) as parameters to test functions?
I\'m testing an object th
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)
I see your problem but I'm not sure about the solution. The problem:
Your functions thing1 and thing2 contain yield statements. When you call a function like that, the returned value is a "generator object." It's an iterator - a sequence of values, which is of course not the same thing as the first value of yield
, or any one particular value.
Those are the objects being passed to your test_attr
function. The test environment is doing that for you automagically, or at least I think that's how it works.
What you really want is the object created in your yield
expression, in other words, Thing(datadir=datadir, errorfile=errorfile)
. There are three ways to get a generator to emit its individual values: by calling next(iter)
, by calling iter.__next__()
or by using the iterator in a loop with an in
expression.
One possibility is to iterate the generator once. Like this:
def test_attr(thing):
first_thing = next(thing)
print(first_thing.datadir)
assert os.path.exists(first_thing.datadir)
first_thing
will be the object you want to test, i.e., Thing(datadir=datadir, errorfile=errorfile)
.
But this is only the first hurdle. The generator function is not finished. Its internal "program counter" is just after the yield
statement. So you haven't exited the context manager and haven't deleted your temporary directory yet. To do this you must call next(thing)
again and catch a StopIteration
exception.
Alternatively I think this will work:
def test_attr(thing):
for a_thing in thing:
print(a_thing.datadir)
assert os.path.exists(a_thing.datadir)
The in
expression loops through all the items in the iterator (there's only one) and exits gracefully when StopIteration occurs. The function exits from the context manager and your work is done.
To me it's an open question whether this makes your code more or less readable and maintainable. It's a bit clumsy.
Temporary directories and files are handled by pytest using the built in fixtures tmpdir and tmpdir_factory.
For this usage, tmpdir should be sufficient: https://docs.pytest.org/en/latest/tmpdir.html
Also, paramertrized fixtures would work well for this example.
These are documented here: https://docs.pytest.org/en/latest/fixture.html#fixture-parametrize
import os
import pytest
class Thing:
def __init__(self, datadir, errorfile):
self.datadir = datadir
self.errorfile = errorfile
@pytest.fixture(params=(1, 2))
def thing(request, tmpdir):
errorfile_name = 'testlog{}.log'.format(request.param)
errorfile = tmpdir.join(errorfile_name)
return Thing(datadir=str(tmpdir), errorfile=str(errorfile))
def test_attr(request, thing):
assert os.path.exists(thing.datadir)
BTW, In Python Testing with pytest, parametrized fixtures are covered in ch3. tmpdir and other built in fixtures are covered in ch4.