Using the unittest Framework¶
Estimated time to read: 4 minutes
Now that we know what a test must look like, let's dive into unit testing with Python. As Python comes "with batteries included", we don't need to install anything to start writing tests. Let's continue with our earlier example.
def square(a):
"""Calculate the surface of a square."""
return a * a
def test_square():
"""Test our ``square()`` function."""
assert square(2) == 4
assert square(0) == 0
assert square(-1) == 1
Separation of Concerns¶
First, we separate the business code and the test code. We want tests to live separately, so we can maintain and execute them better.
In the test module, we must import the example module, obviously. Let's also add two lines that will run the test later, when we execute the module.
"""FILE: test_example.py"""
from example import square
def test_square():
"""Test our ``square()`` function."""
assert square(2) == 4
assert square(0) == 0
assert square(-1) == 1
if __name__ == "__main__":
test_square()
Note
The name guard is a common pattern to avoid code to be executed when the module containing it is imported. When we run the module with the Python interpreter the test function will be called just normally.
Great! Now we can run our test on the command line.
Nothing happens, hmmm. Alright, that means that the test didn't fail.
If we want to see our test failing we can make a change to the code. Let's change one line in the test module to make the test fail, e.g.
When we now execute our test...
$ python test_example.py
Traceback (most recent call last):
File "/home/biella/example/test_example.py", line 12, in <module>
test_square()
File "/home/biella/example/test_example.py", line 6, in test_square
assert square(2) == 42
AssertionError
Okay, that's what it looks like when our test reports a wrong result.
Using a Test Framework¶
Running tests like this works, but it's not particularly beautiful. The execution will abort at the first detected issue. Also, no summary of the test results will be printed. This is why test frameworks exist! Let's beef up our test setup with the Python standard library's unittest framework.
"""FILE: test_example.py"""
import unittest
from example import square
class ExampleTests(unittest.TestCase):
def test_square(self):
"""Test our ``square()`` function."""
assert square(2) == 4
assert square(0) == 0
assert square(-1) == 1
if __name__ == "__main__":
unittest.main()
The unittest framework wants us to use classes. A TestCase class is a container for an arbitrary number of tests on the same or closely related topic. Now, we can run our test in the terminal as usual:
$ python test_example.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Note the small period above the long divider line. This represents our test. Every successful test will be depicted as a . character, every failing test will show up with a capital F. We can use the -v option for a more verbose output, which will show the test names instead:
$ python test_example.py -v
test_square (test_example.ExampleTests)
Test our ``square()`` function. ... ok
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
The -v option is handled by unittest.main(), i.e. the unittest framework.
The framework can do much more, though. Instead of assert it expects us to use the numerous assertXxxx() methods the TestCase class provides. Typically, our code would then look like this:
"""FILE: test_example.py"""
import unittest
from example import square
class ExampleTests(unittest.TestCase):
def test_square(self):
"""Test our ``square()`` function."""
self.assertEqual(square(2), 42)
self.assertEqual(square(0), 0)
self.assertEqual(square(-1), 1)
We can omit calling unittest.main() and the use of the name guard if we run unittest as a module. Note that above we have deliberately used a wrong result in the test code, hence when we run our test suite, we get:
$ python -m unittest
F
======================================================================
FAIL: test_square (test_example.ExampleTests)
Test our ``square()`` function.
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/biella/example/test_example.py", line 9, in test_square
self.assertEqual(square(2), 42)
AssertionError: 4 != 42
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (failures=1)
The assertEqual boilerplate method tries to give a slightly more helpful output than the plain assert when an error occurs.
Note that we didn't call our test module directly. unittest walked through the directory tree and collected the test all by itself. By default, the test discovery algorithm looks for files like test*.py and starts its exploration at the execution directory.
Note
What we call from the terminal is the unittest command line interface (CLI). It provides an additional discover command, which gives greater control over the test discovery behavior. You can use the --help option, e.g. python -m unittest --help, to display more CLI usage information, commands and options.
Can we do better? We want pythonic code!¶
One last thing: Does this code look more readable than it did at the beginning? No, probably not. The code looks bloated, it is not pythonic. This is because unittest was inspired by JUnit, the unit testing framework for Java. Can we do better than that? Yes, we can. Let's take a look at that in the next chapter.