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:
python test.py
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:
python test.py mytest
or:
python test.py mypkg/mytest
If you only want to display the first error, use:
python test.py -1
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
