Skip to content

Instantly share code, notes, and snippets.

@wrobstory
Last active October 20, 2015 22:21
Show Gist options
  • Save wrobstory/dd48df7143a4a4083f54 to your computer and use it in GitHub Desktop.
Save wrobstory/dd48df7143a4a4083f54 to your computer and use it in GitHub Desktop.
PyTest4Tim

Why py.test?

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.

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.

@thisfred
Copy link

thisfred commented Aug 3, 2015

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.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment