Testing Python Programs
Testing is a fundamental part of software development and web software development is no different. We need to be sure that the code we write does what we expect and continues to do so as changes are made to the implementation. While it's possible to test code by hand, automated unit testing is a less error prone way of running a set of tests and validating the results.
There are two styles of testing available as part of the standard Python
library. The doctest
module implements a simple form of test that are
written as part of the documentation for a procedure. The unittest
module provides a more complex style of testing using clases similar to
the JUnit framwork for Java. This chapter will describe both styles of
testing and give examples of how they can be used in program
development.
Easy Testing with doctest
Perhaps the easiest way to start testing in Python is using the
doctest module. Writing
tests with doctest
is a matter of capturing the way that you call your
procedures and the results that you expect in the interactive Python
shell. The commands and their output are recorded in the documentation
string for the procedure to be tested, and so form part of the
documentation for the procedure - this in itself is often quite useful
as it illustrates how your code can be used.
Let's work through an example of writing and testing a procedure compute the power of a number. The procedure will have the following signature and documentation string:
def power(x, y=2):
"""Return x to the power of y, y defaults
to 2 if not provided"""
pass
Note that the word pass
here is a Python statement that does nothing,
it's used, as here, where we need a statement of some kind to make the
code compile, but we don't want to do anything (or don't know what to do
yet). We'll replace pass
with the actual implementation later.
Before we write the code, the Test Driven Development methodology says
that we should write the tests. For doctest
, we write out what we'd
expect if we were to call the procedure in an interactive Python
session:
>>> power(2)
4
>>> power(4)
16
>>> power(2, 3)
8
In this example I've written out a few test cases with the expected results. To turn this into a test, we add the examples to the documentation string for the procedure:
def power(x, y=2):
"""Return x to the power of y, y defaults
to 2 if not provided
>>> power(2)
4
>>> power(4)
16
>>> power(2, 3)
8
"""
pass
To run the tests we include the following lines at the end of the module (file) contianing the code and tests:
if __name__ == "__main__":
import doctest
doctest.testmod()
If this module is run (either on the command line or via Eclipse), the output is as follows:
> python3 power.py
**********************************************************************
File "power.py", line 5, in __main__.power
Failed example:
power(2)
Expected:
4
Got nothing
**********************************************************************
File "power.py", line 7, in __main__.power
Failed example:
power(4)
Expected:
16
Got nothing
**********************************************************************
File "power.py", line 9, in __main__.power
Failed example:
power(2, 3)
Expected:
8
Got nothing
**********************************************************************
1 items had failures:
3 of 3 in __main__.power
***Test Failed*** 3 failures.
As you can see, all of the tests failed and a message is shown for each
about what test is being run and what went wrong. Now, I can write code
to make the tests pass. Now one way to implement this procedure to pass
the first two tests would be to return the square of the input x
:
def power(x, y=2):
"""Return x to the power of y, y defaults
to 2 if not provided
>>> power(2)
4
>>> power(4)
16
>>> power(2, 3)
8
"""
return x*x
Now, if we re-run the module, the output is:
> python3 power.py
**********************************************************************
File "power.py", line 9, in __main__.power
Failed example:
power(2, 3)
Expected:
8
Got:
4
**********************************************************************
1 items had failures:
1 of 3 in __main__.power
***Test Failed*** 1 failures.
Now only one test is failing, the first two tests that are asking for
the square of the input generate no output. Sometimes it's useful to see
some output from the passing tests (if only for reassurance that
something is happening). To do this we can add the -v
option on the
command line:
> python3 power.py -v
Trying:
power(2)
Expecting:
4
ok
Trying:
power(4)
Expecting:
16
ok
Trying:
power(2, 3)
Expecting:
8
**********************************************************************
File "power.py", line 9, in __main__.power
Failed example:
power(2, 3)
Expected:
8
Got:
4
1 items had no tests:
__main__
**********************************************************************
1 items had failures:
1 of 3 in __main__.power
3 tests in 2 items.
2 passed and 1 failed.
***Test Failed*** 1 failures.
As I complete the implementation of power
I can re-run the tests to
check that my implementation matches my expectations. Once it does, the
output (without the -v
option) should be empty; I can then move on to
the next piece of work.
If I later discover a bug in the code, the tests can be updated to reproduce the bug and help me correct it. For example, if I find that when I call the function with the second argument greater than 5, the wrong result is returned, I can add a new line to the tests:
def power(x, y=2):
"""Return x to the power of y, y defaults
to 2 if not provided
>>> power(2)
4
>>> power(4)
16
>>> power(2, 3)
8
>>> power(2, 6)
64
"""
...
Now I can run the tests and I expect the new one to fail - this reproduces the bug. I can now work on fixing it, safe in the knowledge that when the test passes, the bug will be fixed. For completeness I would probably add more than one test to illustrate the bug (eg. one even and one odd value).
Unit Testing with unittest
The second style of tests in Python is implemented using the unittest
module. In this case, tests are written separately to the code being
tested, but the idea that we are calling the procedures under test and
checking the results remains.
A unit test in Python is a class with one or more methods with names
starting with test
. The test methods will generally call the code
under test and make assertions about the results. A test runner runs all
of the test methods and reports the results as pass/fail including any
messages generated in the process. To illustrate, here's a simple
example of a unit test.
import unittest
class Test(unittest.TestCase):
def testNothing(self):
"""Illustrate the unit testing framework"""
s = "this is a string"
words = s.split()
self.assertEqual(len(words), 4, "expected four words")
if __name__ == "__main__":
unittest.main()
In this example we import the unittest
module and define a subclass of
unittest.TestCase
to contain our test methods. We define one test
method as an example which splits a string into words and then tests
whether the length of the result is 4. The assertion is written as a
call to self.assertEqual
which takes three arguments, the first two
are values that are supposed to be the same, the third is a message to
display if the assertion fails.
The final part of the test file is the main section which calls
unittest.main()
. This call finds all of the tests in the file and runs
them, reporting the results. There are other ways of running tests but
this is the simplest method.
If you execute this file from the command line, you'll see the following output:
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
The test we wrote passed so we see no output other than the number of tests that were run. If you are working in Eclipse, you can run your test files in a slightly different way. From the "Run" menu choose "Run As..." and select "Python unit-test". This will run your tests and show the output above, but also give you a PyUnit tab next to the console with a breakdown of the tests and a few handy controls. In the screenshot below you can see the result of running the simple tests above, since everything passed I get the green bar, if there had been failures the bar would be red and there would be details shown of the errors or failure messages.
import unittest
class Test(unittest.TestCase):
def testNothing(self):
"""Illustrate the unit testing framework"""
s = "this is a string"
words = s.split()
self.assertEqual(len(words), 4, "expected four words")
self.assertTrue(3==4, "three does not equal four")
if __name__ == "__main__":
unittest.main()
======================================================================
FAIL: testNothing (testfail.Test)
Illustrate the unit testing framework
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/steve/workspace/practasks/src/testfail.py", line 13, in testNothing
self.assertTrue(3==4, "three does not equal four")
AssertionError: three does not equal four
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (failures=1)
def power(x, y=2):
"""Return x to the power of y, y defaults
to 2 if not provided"""
pass
import unittest
# import the procedure we're testing
from power import power
class Test(unittest.TestCase):
def testPowerDefaultValue(self):
"""Does the exponent argument properly default to 2"""
self.assertEqual(4, power(2), "failure with default arg")
self.assertEqual(16, power(4), "failure with default arg")
def testPowerVariousExponents(self):
"""Test various values of exponent for power"""
self.assertEqual(1, power(10, 0), "10^0 should be 1")
self.assertEqual(10, power(10, 1), "10^1 should be 10")
self.assertEqual(100000000000000000000, power(10, 20), "10^20 should be very large")
if __name__ == "__main__":
unittest.main()
> python power_test.py
FF
======================================================================
FAIL: testPowerDefaultValue (__main__.Test)
Does the exponent argument properly default to 2
----------------------------------------------------------------------
Traceback (most recent call last):
File "power_test.py", line 11, in testPowerDefaultValue
self.assertEqual(4, power(2), "failure with default arg")
AssertionError: failure with default arg
======================================================================
FAIL: testPowerVariousExponents (__main__.Test)
Test various values of exponent for power
----------------------------------------------------------------------
Traceback (most recent call last):
File "power_test.py", line 17, in testPowerVariousExponents
self.assertEqual(1, power(10, 0), "10^0 should be 1")
AssertionError: 10^0 should be 1
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=2)
> python power_test.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK