Skip to content

Commit

Permalink
Merge pull request #4 from zopefoundation/doctestcase
Browse files Browse the repository at this point in the history
doctests in TestCase classes
  • Loading branch information
jimfulton committed Jul 15, 2015
2 parents e137978 + d0353c9 commit 3d93ea5
Show file tree
Hide file tree
Showing 11 changed files with 481 additions and 2 deletions.
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
Changes
=======

- Added support for creating doctests as methods of
``unittest.TestCase`` classes so that they can found automatically
by test runners, like *nose* that ignore test suites.

4.2.1 (unreleased)
------------------

Expand Down
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ wait
A small utility for dealing with timing non-determinism
See wait.txt.

doctestcase
Support for defining doctests as methods of ``unittest.TestCase``
classes so that they can be more easily found by test runners, like
nose, that ignore test suites.

.. contents::

Getting started
---------------

Expand Down
8 changes: 7 additions & 1 deletion buildout.cfg
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
[buildout]
develop = .
parts = test
parts = py
unzip = true

[test]
recipe = zc.recipe.testrunner
eggs =
zope.testing

[py]
recipe = zc.recipe.egg
eggs = ${test:eggs}
nose
interpreter = py
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def read(*rnames):
'renormalizing.txt',
'setupstack.txt',
'wait.txt',
'doctestcase.txt',
]]

long_description='\n\n'.join(
Expand Down
224 changes: 224 additions & 0 deletions src/zope/testing/doctestcase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
r"""Doctests in TestCase classes
The original ``doctest`` unittest integration was based on
``unittest`` test suites, which have fallen out of favor. This module
provides a way to define doctests inside of unittest ``TestCase``
classes. It also provides better integration with unittest test
fixtures, because doctests use setup provided by the containing test
case class. It also provides access to unittest assertion
methods.
You can define doctests in 4 ways:
- references to named files
- strings
- decorated functions with docstrings
- reference to named files decorating test-specific setup functions
.. some setup
>>> __name__ = 'tests'
Here are some examples::
from zope.testing import doctestcase
import doctest
import unittest
g = 'global'
class MyTest(unittest.TestCase):
def setUp(self):
self.a = 1
self.globs = dict(c=9)
test1 = doctestcase.file('test1.txt', optionflags=doctest.ELLIPSIS)
test2 = doctestcase.docteststring('''
>>> self.a, g, c
(1, 'global', 9)
''')
@doctestcase.doctestmethod(optionflags=doctest.ELLIPSIS)
def test3(self):
'''
>>> self.a, self.x, g, c
(1, 3, 'global', 9)
'''
self.x = 3
@doctestcase.doctestfile('test4.txt')
def test4(self):
self.x = 5
In this example, 3 constructors were used:
doctestfile (alias: file)
doctestfile makes a file-based test case.
This can be used as a decorator, in which case, the decorated
function is called before the test is run, to provide test-specific
setup.
docteststring (alias string)
docteststring constructs a doctest from a string.
doctestmethod (alias method)
doctestmethod constructs a doctest from a method.
The method's docstring provides the test. The method's body provides
optional test-specific setup.
Note that short aliases are provided, which may be useful in certain
import styles.
Tests have access to the following data:
- Tests created with the ``docteststring`` and ``doctestmethod``
constructors have access to the module globals of the defining
module.
- In tests created with the ``docteststring`` and ``doctestmethod``
constructors, the test case instance is available as the ``self``
variable.
- In tests created with the ``doctestfile`` constructor, the test case
instance is available as the ``test`` variable.
- If a test case defines a globs attribute, it must be a dictionary
and its contents are added to the test globals.
The constructors accept standard doctest ``optionflags`` and
``checker`` arguments.
Note that the doctest IGNORE_EXCEPTION_DETAIL option flag is
added to optionflags.
"""
import doctest
import inspect
import os
import re
import sys
import types

__all__ = ['doctestmethod', 'docteststring', 'doctestfile']

_parser = doctest.DocTestParser()

def doctestmethod(test=None, optionflags=0, checker=None):
"""Define a doctest from a method within a unittest.TestCase.
The method's doc string provides the test source. Its body is
called before the test and may perform test-specific setup.
You can pass doctest option flags and a custon checker.
Variables defined in the enclosing module are available in the test.
If a test case defines a globs attribute, it must be a dictionary
and its contents are added to the test globals.
The test object is available as the variable ``self`` in the test.
"""
if test is None:
return lambda test: _doctestmethod(test, optionflags, checker)

return _doctestmethod(test, optionflags, checker)

method = doctestmethod

def _doctestmethod(test, optionflags, checker):
doc = test.__doc__
if not doc:
raise ValueError(test, "has no docstring")
setup = test
name = test.__name__
path = inspect.getsourcefile(test)
lineno = inspect.getsourcelines(test)[1]

fglobs = sys._getframe(3).f_globals

def test_method(self):
setup(self)
_run_test(self, doc, fglobs.copy(), name, path,
optionflags, checker, lineno=lineno)

return test_method

def docteststring(test, optionflags=0, checker=None):
"""Define a doctest from a string within a unittest.TestCase.
You can pass doctest option flags and a custon checker.
Variables defined in the enclosing module are available in the test.
If a test case defines a globs attribute, it must be a dictionary
and its contents are added to the test globals.
The test object is available as the variable ``self`` in the test.
"""
fglobs = sys._getframe(2).f_globals

def test_string(self):
_run_test(self, test, fglobs.copy(), '<string>', '<string>',
optionflags, checker)

return test_string

string = docteststring

def doctestfile(path, optionflags=0, checker=None):
"""Define a doctest from a test file within a unittest.TestCase.
The file path may be relative or absolute. If its relative (the
common case), it will be interpreted relative to the directory
containing the referencing module.
You can pass doctest option flags and a custon checker.
If a test case defines a globs attribute, it must be a dictionary
and its contents are added to the test globals.
The test object is available as the variable ``self`` in the test.
The resulting object can be used as a function decorator. The
decorated method is called before the test and may perform
test-specific setup. (The decorated method's doc string is ignored.)
"""
base = os.path.dirname(os.path.abspath(
sys._getframe(2).f_globals['__file__']
))
path = os.path.join(base, path)
with open(path) as f:
test = f.read()
name = os.path.basename(path)

def test_file(self):
if isinstance(self, types.FunctionType):
setup = self
def test_file_w_setup(self):
setup(self)
_run_test(self, test, {}, name, path, optionflags, checker,
'test')

return test_file_w_setup

_run_test(self, test, {}, name, path, optionflags, checker, 'test')

return test_file

file = doctestfile

def _run_test(self, test, globs, name, path,
optionflags, checker, testname='self', lineno=0):
globs.update(getattr(self, 'globs', ()))
globs[testname] = self
optionflags |= doctest.IGNORE_EXCEPTION_DETAIL
doctest.DocTestCase(
_parser.get_doctest(test, globs, name, path, lineno),
optionflags=optionflags,
checker=checker,
).runTest()
Loading

0 comments on commit 3d93ea5

Please sign in to comment.