Testing with doctest

By example I've illustrated a large number of ways of writing testing harnesses for code during this course. The way they were constructed depended on the kinds of tests needed to verify the code works.

Python provides a nice built-in mechanism that makes it easy to express tests for many kinds of programs and produces useful documentation at the same time. The doctest module lets you include test specifications in docstrings and then automatically extracts and uses them and reports on the results. The rationale behind it is roughly that (i) we all agree that documentation is good, and (ii) we all agree that testing is good, so, why not combine the two and have the tests supplement the documentation? This has the added benefit of keeping the tests right with the code they test and not somewhere else in the module.

Here's a familiar bit of code with tests in it and a main routine that will run and report on the tests:

# WumpusAdjMatrixMap.py
#
# Adjacency matrix representation of a cave_system.
# A 1 at entry [i, j] indicates that you can get from room i to room j.
#
# Note that this cave_system is not a dodecahedron and that some tunnels
# are 1-way.
cave_system = []
cave_system.append([0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
cave_system.append([1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0])
cave_system.append([0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0])
cave_system.append([0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0])
cave_system.append([0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1])
cave_system.append([0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0])
cave_system.append([0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0])
cave_system.append([0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0])
cave_system.append([0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1])
cave_system.append([0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
cave_system.append([0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0])
cave_system.append([1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0])
cave_system.append([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1])
cave_system.append([0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0])
cave_system.append([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0])
cave_system.append([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1])
cave_system.append([0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0])
cave_system.append([1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0])
cave_system.append([0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1])
cave_system.append([0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0])

def tunnels_from(cave_map, room_number):
    ''' returns a list of the rooms that can be reached from room room_number.
    >>> tunnels_from(cave_system, 0)
    [1, 4, 7]
    '''
    pass

def tunnels_to(cave_map, room_number):
    ''' returns a list of the rooms that have tunnels leading to room room_number.
    >>> tunnels_to(cave_system, 0)
    [1, 11, 14, 17]
    '''
    pass

if __name__=='__main__':
    import doctest
    doctest.testmod()

The testmod method will extract the lines in the docstrings that look like interactive python sessions, i.e. the ones beginning with >>> and the lines following them. It will execute the commands after the >>>s and compare their output with the lines following them. If the actual output matches the specified output it counts as a success. If they do not match it counts as a failure, and reports on it. For example here is the output from executing the module above:

>>> 
**********************************************************************
File "__main__", line 32, in __main__.tunnels_from
Failed example:
    tunnels_from(cave_system, 0)
Expected:
    [1, 4, 7]
Got nothing
**********************************************************************
File "__main__", line 39, in __main__.tunnels_to
Failed example:
    tunnels_to(cave_system, 0)
Expected:
    [1, 11, 14, 17]
Got nothing
**********************************************************************
2 items had failures:
   1 of   1 in __main__.tunnels_from
   1 of   1 in __main__.tunnels_to
***Test Failed*** 2 failures.
>>> 

You can see that the output shows what it expected to get and what it actually got test by test, and at the end it summarizes the failures. testmod's default setting is to be silent on successes so had there been successful tests they would have generated no output.

The strengths of the doctest approach to testing are that:

The downside to the doctest approach is just that not all methods lend themselves to being run in a shell session, and that is doctest's only paradigm. It also can't help you catch runtime errors in interactive programs, e.g. problems with bat snatching in Hunt the Wumpus.

On balance it is best to think about doctest as a good starting point, but not necessarily sufficient on its own.

You can read more about doctest in Section 26.2 of the Python documentation.