Fixtures
Overview
Teaching: 10 min
Exercises: 0 minQuestions
How do I create and cleanup the data I need to test the code?
Objectives
Understand how test fixtures can help write tests.
Setup and Teardown
The above example didn’t require much setup or teardown. Consider, however, the
following example that could arise when communicating with third-party programs.
You have a function f()
which will write a file named yes.txt
to disk with
the value 42 but only if a file no.txt
does not exist. To truly test that the
function works, you would want to ensure that neither yes.txt
nor no.txt
existed before you ran your test. After the test, you want to clean up after
yourself before the next test comes along. You could write the test, setup,
and teardown functions as follows:
import os
from mod import f
def f_setup():
# The f_setup() function tests ensure that neither the yes.txt nor the
# no.txt files exist.
files = os.listdir('.')
if 'no.txt' in files:
os.remove('no.txt')
if 'yes.txt' in files:
os.remove('yes.txt')
def f_teardown():
# The f_teardown() function removes the yes.txt file, if it was created.
files = os.listdir('.')
if 'yes.txt' in files:
os.remove('yes.txt')
def test_f():
# The first action of test_f() is to make sure the file system is clean.
f_setup()
exp = 42
f()
with open('yes.txt', 'r') as fhandle:
obs = int(fhandle.read())
assert obs == exp
# The last action of test_f() is to clean up after itself.
f_teardown()
The above implementation of setup and teardown is usually fine.
However, it does not guarantee that the f_setup()
and the f_teardown()
functions will be called. This is because an unexpected error anywhere in
the body of f()
or test_f()
will cause the test to abort before the
teardown function is reached.
These setup and teardown behaviors are needed when test fixtures must be created. A fixture is any environmental state or object that is required for the test to successfully run.
As above, a function that is executed before the test to prepare the fixture
is called a setup function. One that is executed to mop-up side effects
after a test is run is called a teardown function.
By giving our setup and teardown functions special names pytest will
ensure that they are run before and after our test function regardless of
what happens in the test function.
Those special names are setup_function
and teardown_function
,
and each needs to take a single argument: the test function being run
(in this case we will not use the argument).
import os
from mod import f
def setup_function(func):
# The setup_function() function tests ensure that neither the yes.txt nor the
# no.txt files exist.
files = os.listdir('.')
if 'no.txt' in files:
os.remove('no.txt')
if 'yes.txt' in files:
os.remove('yes.txt')
def teardown_function(func):
# The f_teardown() function removes the yes.txt file, if it was created.
files = os.listdir('.')
if 'yes.txt' in files:
os.remove('yes.txt')
def test_f():
exp = 42
f()
with open('yes.txt', 'r') as fhandle:
obs = int(fhandle.read())
assert obs == exp
The setup and teardown functions make our test simpler and the teardown function is guaranteed to be run even if an exception happens in our test. In addition, the setup and teardown functions will be automatically called for every test in a given file so that each begins and ends with clean state.
Pytest Fixtures
Currently these tests are creating and removing files in the current directory:
while it’s unlikely you have a yes.txt
or no.txt
in your current directory,
tests should not assume that they can change files in the current directory.
What we should do is use a temporary directory. Python provides a module called
tempfile
, which pytest has
nicely integrated into their testing system so that we can get a new temporary
(and empty) directory for each test.
from mod import f
def test_f(tmpdir):
exp = 42
f(tmpdir)
with open(tmpdir.join('yes.txt'), 'r') as fhandle:
obs = int(fhandle.read())
assert obs == exp
Note that we’ve only tested when there is no no.txt
file. Let’s add that test:
import os.path
from mod import f
def test_f(tmpdir):
exp = 42
f(tmpdir)
with open(tmpdir.join('yes.txt'), 'r') as fhandle:
obs = int(fhandle.read())
assert obs == exp
def test_f_with_no(tmpdir):
with open(tmpdir.join('no.txt'), 'x'):
pass
f(tmpdir)
assert not os.path.exists(tmpdir.join('yes.txt'))
We can also create our own pytest fixtures, but we’re going to need to learn about some new Python syntax.
Decorators
You may have seen the following bit of python code before
@something def something_else(): pass
The
@something
is called a decorator, and this decorator is modifying the function below it (in this casesomething_else
. pytest uses decorators widely, e.g. you can tell pytest to skip a test with thepytest.mark.skip
decorator. See this stackexchange answer for further information about how decorators work in Python.
To create a fixture which ensures we have a no.txt
, we need to import pytest,
and then use the pytest.fixture
decorator
import os.path
import pytest
from mod import f
@pytest.fixture
def no_txt_dir(tmpdir):
with open(tmpdir.join('no.txt'), 'x'):
pass
return tmpdir
def test_f(tmpdir):
exp = 42
f(tmpdir)
with open(tmpdir.join('yes.txt'), 'r') as fhandle:
obs = int(fhandle.read())
assert obs == exp
def test_f_with_no(no_txt_dir):
f(no_txt_dir)
assert not os.path.exists(no_txt_dir.join('yes.txt'))
Key Points
It may be necessary to set up “fixtures” composing the test environment.