The test suite to accompany factorial.py
is called test_factorial.py
. Run it and you should see output similar to this:
python test_factorial.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.000s
OK
Three tests were executed and apparently everything went ok.
The code in test_factorial.py
starts by importing both the module we want to test (factorial) and the unittest
module. Then we define a single class named Test
(highlighted) derived from unittest.TestCase
. By deriving from this class, our class will be distinguishable as a test case to the test runner and will provide us with a number of assertion methods.
Our Test
class may consist of any number of methods. The ones with names starting with test_
will be recognized as tests by the test runner. Because the names of failing tests will be printed, it is useful to give these tests sensible names reflecting their purpose. Here we define three such methods: test_number(), test_zero()
, and test_illegal()
.
Chapter4/test_factorial.py
import unittest
from factorial import fac
class Test(unittest.TestCase):
def test_number(self):
self.assertEqual(24,fac(4))
self.assertEqual(120,fac(5))
self.assertEqual(720,fac(6))
def test_zero(self):
self.assertEqual(1,fac(0))
def test_illegal(self):
with self.assertRaises(ValueError):
fac(-4)
with self.assertRaises(ValueError):
fac(3.1415)
if __name__ == '__main__':
unittest.main()
test_number()
tests a number of regular cases to see if our function returns something reasonable. In this case, we check three different numbers and use the assertEquals()
method inherited from the TestCase
class to check that the value calculated (passed as the second argument) equals the expected value (the first argument).
test_zero()
asserts that the special case of zero indeed returns 1. It again uses the assertEqual()
method to check whether the expected value (1) matches the value returned.
test_illegal()
finally asserts that only positive arguments are accepted (or rather it asserts that negative values correctly raise a ValueError
exception) and that arguments to fac()
should be int
or raise a ValueError
as well.
It utilizes the method assertRaises()
provided by TestCase. assertRaises()
will return an object that can be used as a context manager in a with statement. Effectively, it will catch any exception and check whether it is an expected one. If not, it will flag the test as failed.
These methods show a familiar pattern in unit testing: a fairly small number of tests check whether the unit behaves correctly in normal cases, while the bulk of the tests are often devoted to special cases (often referred to as edge cases). And, just as important, serious effort is spent on testing that illegal cases are correctly flagged as such.
The last thing we find in test_factorial.py
is a call to unittest.main()
, the test runner. It will look for any defined classes deriving from TestCase
and run any method that starts with test_
, tallying the results.
If we would change, for example, the implementation of fac()
to something that does not use recursion like the following code, we could rapidly check that it behaves as expected by running test_factorial.py
again.
from functools import reduce
def fac(n):
if n < 0 : raise ValueError("factorial of a negative number is not
defined")
if type(n) != int : raise ValueError("argument is not an integer")
if n == 0 : return 1
if n == 1 : return 1
return reduce(lambda x,y:x*y,range(3,n+1))
The special case handling remains the same, but the highlighted line shows that we now calculate the factorial with Python's reduce()
function from the functools
module. The reduce()
function will apply a function to the first pair of items in a list and then again to the result of this and each remaining item. The product of all numbers in a list can be calculated by passing reduce()
a function that will return the product of two arguments, in this case, our lambda function.
More on the reduce()
function can be found in the documentation of the functools
module, Python's powerful functional programming library: http://docs.python.org/py3k/library/functools.html.
18.226.163.229