Automated testing with doctest

One of the ways to make your code more robust to unexpected input is to develop with boundary cases in your mind. Test-driven code development begins with writing a set of unit tests for each class. These tests often includes normal and extreme use cases. Thanks to packages like doctest for Python, Mocha and Jasmine for Javascript etc., we can write and test codes with an easy format. In this blog post, I will present a short example of how to get started with doctest in Python. N.B. doctest is best suited for small tests with a few scripts. If you would like to run a system testing, look for some other packages!

Assume you have a small function that turns a set of input into an array of 3D coordinates:

import numpy as np
def Point3D(*args):
    return np.array(args,dtype=np.float64)

If you’d like to ensure that only three numbers are fed into the input:

def Point3D(*args):
    if len(args) != 3:
        print('Not a point in 3D')
        return;
    else:
        return np.array(args,dtype=np.float64)

Even more, if you catch a non-floating point input:

def Point3D(*args):
    if len(args) != 3:
        print('Not a point in 3D')
        return;
    else:
        try:
            return np.array(args,dtype=np.float64)
        except:
            print('Cannot be converted to float')

To incorporate some testing to see that you’re catching it correctly, I have the following three test cases:

Input Expected output
Point3D(0,0,0) array([0., 0., 0.])
Point3D(0,2) Not a point in 3D
Point3D(0,’A’,0) Cannot be converted to float

doctest reads the multiline string between the function definition and the first line of the function. You preceed the input with >>> and the output in the subsequent line:

import numpy as np
def Point3D(*args):
    """
    >>> Point3D(0,0,0)
    array([0., 0., 0.])
    >>> Point3D(0,2)
    Not a point in 3D
    >>> Point3D(0,'A',0)
    Cannot be converted to float
    """
    if len(args) != 3:
        print('Not a point in 3D')
        return;
    else:
        try:
            return np.array(args,dtype=np.float64)
        except:
            print('Cannot be converted to float')

To run this:

def runDoctests():  
    import sys
    import doctest
     failed, _ = doctest.testmod(optionflags=doctest.ELLIPSIS, verbose=None)
    sys.exit(failed)


if __name__ == '__main__': 
    runDoctests()

It should exit normally if all your tests have been passed. If any of the test fails (see the example below), it will give you a report on where it fails.

Example code with unexpected output (should have been ‘Cannot be converted to float’ instead of ‘Not a point in 3D’ ):

def Point3D(*args):
    """
    >>> Point3D(0,'A',0)
    Not a point in 3D
    """
    if len(args) != 3:
        print('Not a point in 3D')
        return;
    else:
        try:
            return np.array(args,dtype=np.float64)
        except:
            print('Cannot be converted to float')

The corresponding report from this code:

**********************************************************************
File "/location/test_doctest.py", line 5, in __main__.Point3D
Failed example:
    Point3D(0,'A',0)
Expected:
    Not a point in 3D
Got:
    Cannot be converted to float
**********************************************************************
1 items had failures:
   1 of   1 in __main__.Point3D
***Test Failed*** 1 failures.
An exception has occurred, use %tb to see the full traceback.

Here you go – a simple doctest demonstration that checks for the right kind of inputs and prints informative error messages

Author