My Python testing set up
To follow up on the last post about testing for science and data analytics, I thought it’d be nice to talk about the specific tools I use for testing. I don’t claim this is the best way to do things, but it tends to work pretty well for small projects and teams.
The py.test tool is handy for running a suite of tests in python. It’s a very general tool for running all the tests in a project. It can do a lot of powerful things, but requires almost no boilerplate to get started with. The pattern I’ve always followed is: Add a
tests directory the root of the project. Name any test files in that directory
test_xyz.py and then any tests within that file
def test_abc():. py.test will find all of these automatically and run them. On several projects that I work on, I have tens or hundreds of tests that, in total, take less than a second to run. As a result, I simply run
py.test every time I make a change.
For more happiness, I add a
pytest.ini file to the root of my project:
[pytest] addopts = -s --tb=short
-s causes any text printed by my tests or code to appear on the screen. By default, py.test suppresses printing/logging to standard out. Personally, I don’t like that while debugging.
--tb=short leads to shorter, more concise tracebacks in the case of test failures.
There are a ton of other features available with py.test, like fixtures, fancy assertions and lots of plugins. Here’s a more comprehensive intro to py.test
Golden master tests
Golden master testing doesn’t really require any special infrastructure, but it can be nice to have a coordinated way to generate the keys and specify golden master tests. First, I set up a configuration option in
tests/conftest.py. This is a special file that
py.test uses to load plugins and configuration.
def pytest_addoption(parser): parser.addoption( "--save-golden-masters", action="store_true", help="reset golden master benchmarks" )
Then, what you do with this depends on the project. In one project, where I’m normally compare numpy arrays, I have a decorator that makes setting up a golden master test really simple.
def golden_master(digits = 6): def decorator(test_fnc): try: save = pytest.config.getoption("--save-golden-masters") except AttributeError as e: save = False @wraps(test_fnc) def wrapper(request, *args, **kwargs): result = test_fnc(request, *args, **kwargs) test_name = request.node.name filename = os.path.join('tests', 'golden_masters', test_name + '.npy') if save: np.save(filename, result) correct = np.load(filename) np.testing.assert_almost_equal(result, correct, digits) return wrapper return decorator
To use this:
@golden_master(4) def test_mycomplexfnc(request): return mycomplexfnc()
When I run
py.test --save-golden-masters, this will automatically save the result of the function to
tests/golden_masters/test_mycomplexfnc.npy. Then, the next time I run
py.test, the output of the
mycomplexfnc() will be compared to 4 digits against the old saved version.
Modifying this to work with pandas dataframes or other objects would be relatively simple. It could also be refactored to work with general objects without much effort, but, in my case, all the functions where I don’t have anything better than a golden master test are functions that return arrays.
Should you put the saved golden master test results in version control? I would say yes, since they are necessary for properly testing the code base.
Sometimes I have tests that take a little while to run and I don’t want them to run every single time I run
py.test. So, I add an opt-in option for running slow tests, following the directions on the py.test docs.
pdb + ipdb
I thought I’d just add a note about debuggers, since I’m way more productive with a debugger at hand. When my tests fail and I can’t figure out why, the first step is to use a debugger to really understand the code. The (P)ython (D)e(b)ugger. Also, the (IP)ython (D)e(b)ugger. These are super handy for dropping into some code to check values/step through the code/general debugging! It combines some of the playfulness of jupyter notebooks with python modules.
Just add these two lines:
import ipdb ipdb.set_trace()
import pdb pdb.set_trace()
And then when those lines are run, execution will stop and I’m dropped into a debugging prompt where I can step through the code, examine variable values and other nice things.