DoctestResources

Note that the scripts below require Python 2.4 or better!

setting options in doctest

I came to this page just trying to understand the doctest docs. It's ironic that they are hard to understand. Anyway, Brian's extensive example below helped me solve my much simpler problem.

If you are also here from Googling "doctest options example", just check this out:

def _test():
   import doctest
   myoptionflags = doctest.ELLIPSIS
   doctest.testmod(optionflags=myoptionflags)

if __name__ == "__main__":
   _test()

--mt

Multiple doctests

This script can be used to execute doctest *.txt files in the current directory.

import sys, os, doctest, re

def test():
    # process args
    optionflags = doctest.ELLIPSIS
    pattern = None
    for arg in sys.argv[1:]:
        if arg == '-1':
            optionflags |= doctest.REPORT_ONLY_FIRST_FAILURE
    if len(sys.argv) > 1 and not sys.argv[-1].startswith('-'):
        pattern = re.compile(sys.argv[-1])

    # find all doc tests (files ending in '.txt' in the current dir struct)
    tests = []
    def collectTests(arg, dir, names):
        subdirs = []
        for name in names:
            full = os.path.join(dir, name)
            if name.lower().endswith('txt'):
                if pattern is None or re.search(pattern, full):
                    tests.append(full)
            elif not name.startswith('.') and os.path.isdir(full):
                subdirs.append(name)
        del names[:]
        names.extend(subdirs)
    os.path.walk('.', collectTests, None)

    # run each test
    if tests:
        totalErrors = 0
        totalTests = 0
        for test in tests:
            errors, tests = doctest.testfile(test, optionflags=optionflags)
            totalErrors += errors
            totalTests += tests
        if totalErrors:
            print '%i error(s) out of %i tests' % (totalErrors, totalTests)
        elif totalTests:
            print 'All %i tests passed' % totalTests
        else:
            print 'Nothing tested'
    else:
        print 'Nothing tested'

if __name__ == '__main__':
    test()

Use

A typical use would be:

This would attempt to execute all of the 'txt' files in the current directory structure. Admittedly, this might not be what everyone wants, but all of the txt files in our packages can contain Python doctests.

You can run a specific test using a regex:

or:

If you only want to display the first error, use:

Default Doctest Options

Because I use an ellipsis all the time to ignore memory addresses or tighten up output code, the runner has the doctest.ELLIPSIS set by default. If you want to disable this for a test, use:

  >>> some_test() # doctest: -ELLIPSIS
  ... This string has...an ellipsis.

An Example

Below is an example you can use to start using test.py above.

sample.txt

================
A Sample Doctest
================

Doctests are a nice place to tell a story and test your code at the
same time. I probably overuse 'story', but I find that sometimes
missing in my own work, particularly at the early stage.

So, here's an example of the sample module.

First, we'll import it:

  >>> import sample

The module has a nice function for building a person's full name
out of various parts. Here's a typical example:

  >>> sample.fullName('Joe', 'Black')
  'Joe Black'

We can omit either the first or last names:

  >>> sample.fullName(firstName='Joe')
  'Joe'

  >>> sample.fullName(lastName='Black')
  'Black'

Okay, parenthetical point here. I'm now having to think about where
this silly function would be used. It could be used to calculate a
label for a directory. So, does it make sense to represent first name
and last name in the same way here. If we could see either 'Joe'
or 'Black', how do we know what to call him?

If I was just focussed on laying down a bunch of assertions, I might
have missed this. But since I have to make sense of the tests (because
I'm telling a story that other people have to read), I caught what might 
be a design problem. I find using doctests this way almost *always*
reveals some type of "brain ambiguity" and so I make it a central part 
of my Python development practice.

Let's start again...

Improved Full Name Function
---------------------------

The fullName function is used to calculate a label for a person for
display in a directory or other listing. For this reason, we must
always provide at least a first name:

  >>> sample.improvedFullName('Joe')
  'Joe'

We can add a last name if we have it:

  >>> sample.improvedFullName('Joe', 'Black')
  'Joe Black'

If we omit the first name:

  >>> sample.improvedFullName(lastName='Black')
  Traceback (most recent call last):
  TypeError: improvedFullName() takes at least 1 non-keyword argument (0 given)

Back to the parenthetical...now things are making more sense (in admitedly a very
simple example). I'm also more confident that someone else will have a chance at
figuring this stuff out when I drop off the project.

And the (doc)tests go on...

sample.py

def fullName(firstName='', lastName=''):
    parts = []
    if firstName:
        parts.append(firstName)
    if lastName:
        parts.append(lastName)
    return ' '.join(parts)

def improvedFullName(firstName, lastName=''):
    return fullName(firstName, lastName)

More doctest Tricks

generic file doctest script

If you are running 2.4, you can put your doctests in separate files. Here's a tiny utility program to do that. Unfortunately there doesn't seem to be an obvious way to add it to your path, as doctest insists on module-relative paths. So you'll need a copy of this in your working directory.


#!/usr/bin/python
# note this may be /usr/local/bin/python on OS X 10.3 or 10.4 to get your 2.4 version

# save as filetest.py
# usage:   filetest <doctest file name>

import doctest
from sys import argv
doctest.testfile(argv[1])

using doctest to test compiled code

Here is sayhi.c

#include <stdio.h>

main()
{
printf("hello\n");
}

and here is a doctest for it

from os import popen

def sayhi():
    """
    >>> sayhi()
    hello
    ...
    """
    f = popen("./a.out")
    for line in f.readlines():
       print line

def _test():
   import doctest
   myoptionflags = doctest.ELLIPSIS
   doctest.testmod(optionflags=myoptionflags)

if __name__ == "__main__":
   _test()

doctest from iPython

doctest doesn't work from within iPython, as both override some deep internal magic called sys.displayhook() However, there is an easy workaround:

# mystuff.py

... <stuff to test>

if __name__ == "__main__":
   try:
      __IP      # check if we are in iPython 
   except:   
      _test()

The tests won't run within an iPython session, but you can still invoke them by going through the shell:

In [42]: !python mystuff.py

Have fun!

last edited 2006-04-19 19:15:13 by mt