Writing readable and maintainable unit tests is crucial to the success of your Python project. For Python, the unittest module, nosetests and py.test are the most commonly used framework for writing unit tests, and so when you start a project, if no one takes the decision for you, you will have to choose between the three. Over the years, I have become a huge fan of py.test, a mature and well-maintained testing package for Python. That's why I would like to summarize the reasons for me liking py.test and explain which features make it an indispensable tool for Python development.
Python's Unittest
To grasp what py.test does it is worth to take a look at Python's default module for unit tests which comes with the standard library: unittest. Unittest is an xUnit, which means it is a descendent of the original SUnit, a unit testing frame work for Smalltalk. Smalltalk, like Java, is a language where methods are the only kind of functions available. So for writing a test, one needed to write a test class, and add each test as a method of that class. These test methods in Smalltalk kind of looked like this:
ExampleSetTest>>setUpfull:=Setwith:5with:6ExampleSetTest>>testIncludesselfassert: (fullincludes:5).selfassert: (fullincludes:6)
Python unittest looks kind of similar (it is the port of a port after all):
classTestExampleSet(unittest.TestCase):defsetUp(self):self.full={5,6}deftest_values_in_full(self):self.assertIn(5,self.full)self.assertIn(6,self.full)
The setUp method is what we called the test fixture, it is a method that is called right before executing all the test_* methods. In our case, this is initializing a set. This method is used for common initializations that are needed in every test. Now we just need to run the tests with:
python -m unittest
It seems a bit weird to use classes for grouping test cases, this is more like what I would solve with namespaces in Python. Again, the reason why unittest organizes tests in classes is: because Smalltalk did.
Nevertheless, the unittest module is a proven tool, that generations of programmers have written unit tests with. Let's take a moment to acknowledge that it is a method that is well-respected and it is totally fine to write tests that way.
It just is not the only way.
pytest
Pytest is a test-runner, an extra module to run unittests as easy as:
py.test
As such, it will run your unittest style unit tests like the one we wrote before (which means that you can pretty easily switch to pytest as a runner for an existing project, without having to rewrite your old unit tests). But: it will also run simple functions that start with test_*:
deftest_values_in_full():assert5in{5,6}assert6in{5,6}
If we want to continue using test fixtures, we can use the pytest.fixture decorator:
importpytest@pytest.fixturedeffull():return{5,6}deftest_values_in_full(full):assert5infullassert6infull
Comparing this to the unittest tests this differs mainly in two aspects, the use of fixtures and of assertions.
Instead of manipulating self of a test object, we just write a simple test function and use a fixture that we have declared with a decorator. pytest's fixture system is composable (unlike the more rigid setUp method). We can combine several, different fixtures. Suppose you have one fixture to secure a database connection, and one fixture to obtain a temporary directory. In pytest you can use them as needed in every test individually.
We use plain Python assert statements instead of self.assert* methods. Pytest will inspect them to come up with a readable error message. I cannot emphasize this enough. Instead of self.assertListEqual(a, b), we can just type assert a == b, and assert foo() replaces self.assertTrue(foo()).
What is also great about pytest are the plugins available for it.
pytest-xdist
pytest xdist is a plugin that distributes tests over several processes, which will reduce the runtime of your unit tests. Just:
pip install pytest-xdist py.test -n 4 # for 4 cores
Most books on testing emphasize how important it is to have unittests run fast. With pytest-xdist we have a simple way to cut runtimes significantly.
pytest-cov
With pyest-cov installed, you will get coverage information very easily:
pip install pytest-cov py.test --cov-report html --cov myproject
this will generate a html report in a subdirectory.:
py.test --cov-report term --cov myproject
will write out coverage information to the terminal. This is nothing extraordinary, just very convenient.
Conclusion
py.test is a convenient and reliable test runner and testing framework. Using py.test, test code looks like idiomatic and modern python code. It has a rich plugin infrastructure with tools for paralellized test execution, coverage measurement (and for example tools for pep8/pyflakes checks, etc.).
It is developed outside of the CPython project, which means that you can benefit from improvements in py.test directly, regardless whether you are running legacy python (2.x) or Python 3.
Py.test is actively developed on github and is in my experience a contributor friendly project.