py.test Assertions
IMO, py.test tests read better, because of the assert
magic. When comparing two Python objects, py.test performs introspection on them for the comparison. As the end user, you don't really need to care about that; you just need to care that your test suite is much more readable. Compare the following:
def test_my_thing():
# Assume we make some things we want to compare
assert expected_list == result_list
assert expected_set == result_set
assert expected_dict == result_dict
to:
def test_my_thing():
# Assume we make some things we want to compare
nosetools.assert_list_equal(expected_list, result_list)
nosetools.assert_set_equal(expected_set, result_set)
nosetools.assert_dict_equal(expected_dict, result_dict)
You might prefer the latter. I prefer the former. It's ok to disagree, different people like different software things. That's why it's nice to have many software things to choose from.
py.test Fixturing
This is the huge one for me. It basically gives you dependency injection for setup, teardown, and any other things you need to initialize or mock out; you simply pass them as function argument into your test functions. I honestly can't do a better job that the documentation linked above- I highly recommend at least skimming the examples. For me, this also results in a nicer reading test suite, because my old habit was to wrap all tests in a class that inherited from unittest.TestCase
so that I could do setUpClass
and tearDownClass
. Now every test I write is a function, and all my setup happens in functions decorated with @pytest.fixture
.
py.test Monkeypatching
Some say if you need to monkeypatch, you probably need to restructure your code. I say it occasionally makes my life much easier, and my test suite much faster, and my level of anger down due to CI running tests with anything involving a socket (like a db connection). For example, here's something I wrote recently:
@pytest.fixture
def mock_redshift():
"""Mock the psycopg2 connection"""
mock_cur = MagicMock()
mock_conn = MagicMock()
mock_conn.cursor.return_value = mock_cur
return mock_conn
@pytest.fixture
def shift(monkeypatch, mock_redshift):
"""Patch psycopg2 with connection mocks, return conn"""
monkeypatch.setattr('psycopg2.connect',
lambda *args, **kwargs: mock_redshift)
That's all I needed to mock out the db connection, and then check the mock to ensure the SQL I expected to see was getting constructed correctly.
tl;dr: my test suites went from being classes with verbose assert statements and a bunch of stuff in setUpClass
to nothing more than a bunch of functions, leaning on built-in fixturing and monkeypatching to perform all of my setup. I think my test suites read much more cleanly using py.test, and I generally find it more friendly to use than unittest or nose. I recommend at least giving it a try.
Also extremely nice: Test function parametrization: If you have tons of tests that look alike, py.test will let you write one, and parametrize out the differences. This works really well if you have a mapping of inputs to expected outputs.
For an example, see: https://github.com/thisfred/val/blob/master/tests/test_tp.py#L150
(this should probably have been split out into two tests, one for valid and one for invalid inputs, but it should show the mechanism.)