doctestcase¶
Features¶
Combines
unittest.TestCaseanddoctestInject globals into doctests
Mock doctests
Respect
setUp()andtearDown()Minimalistic decorator-based API
Format docstring as Markdown and reST to include in docs
Naturally fits docsub-based pipeline
No dependencies
Checked with mypy
100% test coverage
Tested with Python 2.7+
Alternatives¶
doctest.DocTestSuiteallows to run doctests withunittest, but individual doctests can’t be extended, parametrized, or enclosed withsetUp–tearDown.
Installation¶
$ pip install doctestcase
$ uv add doctestcase
Use cases¶
Decorated
TestCaseParametrize test case
Reuse
__doctestcase__from otherTestCaseInherit from decorated
TestCaseFormat docstring as Markdown or reStructuredText
Integration with docsub
See API Reference for details.
Decorated TestCase¶
from doctest import ELLIPSIS
from unittest import TestCase
from doctestcase import doctestcase
@doctestcase(globals={'X': 'yz'}, options=ELLIPSIS)
class SimpleCase(TestCase):
"""
Title
Paragraph.
>>> X * 100
'yzyz...'
Another paragraph.
>>> None
>>> True
True
"""
def test_custom(self): # called before 'test_docstring'
self.assertTrue(True)
def test_other(self): # called after 'test_docstring'
self.assertTrue(True)
All test methods are called by unittest in alphabetic order, including test_docstring, added by @doctestcase.
Mock doctests¶
In classes decorated with @doctestcase, unittest.mock patches apply to doctests
too, if patching is applied above @doctestcase() decorator. For Python below 3.3,
use mock package instead.
import time
from unittest import TestCase, mock
from doctestcase import doctestcase
@mock.patch('time.time', mock.MagicMock(return_value=0))
@doctestcase()
class WithPatchedTime(TestCase):
"""
Mocking modules in doctests and testcase methods
>>> import time
>>> time.time()
0
"""
def test_method(self):
self.assertEqual(0, time.time())
Parametrize doctest case¶
First, define base class parametrized with cwd:
from doctest import ELLIPSIS
import os.path
import shutil
import tempfile
from unittest import TestCase
from doctestcase import doctestcase
@doctestcase(options=ELLIPSIS, cwd='.')
class ChdirTestCase(TestCase):
def setUp(self):
if self.__class__ is ChdirTestCase:
self.skipTest('base class') # no tests of the base class itself
self.temp = tempfile.mkdtemp()
self.prev = os.getcwd()
cwd = os.path.join(self.temp, self.__doctestcase__.kwargs['cwd'])
if not os.path.exists(cwd):
os.mkdir(cwd)
os.chdir(cwd)
def tearDown(self):
os.chdir(self.prev)
shutil.rmtree(self.temp)
Notice how the base class is skipped from testing.
In this example we use os.path module for compatibility with older Python versions only. If you use recent Python versions, use pathlib instead.
Now we can define test case parametrized with cwd:
@doctestcase(cwd='subdir')
class Case1(ChdirTestCase):
"""
>>> import os
>>> os.getcwd()
'.../subdir'
"""
Reuse __doctestcase__ from other TestCase¶
Extending example above,
@SimpleCase.__doctestcase__
class AnotherCase(TestCase):
"""
Title
>>> X * 100
'yzyz...'
"""
Now AnotherCase.__doctestcase__ holds shallow copy of globals, kwargs, and same doctest options, as SimpleCase. These copies are independent.
Inherit from decorated TestCase¶
Test cases, decorated with @doctestcase, can be used as base classes for other test cases. This is useful when inherited classes need to extend or change properties, passed to parent’s @doctestcase. Parent properties will be copied and updated with values from child class decorator.
For the SimpleCase class above,
@doctestcase(globals={'A': 'bc'})
class InheritedCase(SimpleCase):
"""
Title
>>> (X + A) * 100
'yzbcyzbc...'
"""
Notice that global variable A was added to globals defined in SimpleCase, and the new class reuses doctest.ELLIPSIS option.
For more details on how doctestcase properties are updated, check the API Reference.
Format docstring as Markdown or reStructuredText¶
For the SimpleCase class above,
Markdown¶
>>> from doctestcase import to_markdown
>>> to_markdown(SimpleCase)
## Title
Paragraph.
```pycon
>>> X * 100
'yzyz...'
```
Another paragraph.
```pycon
>>> None
>>> True
True
```
reStructuredText¶
>>> from doctestcase import to_rest
>>> to_rest(SimpleCase)
Title
-----
Paragraph.
>>> X * 100
'yzyz...'
Another paragraph.
>>> None
>>> True
True
Integration with docsub¶
When documenting packages, “Usage” section often includes doctests. It is a good practice to test all documented use cases, so why not adopt test-driven documenting approach and write tests with docs in mind?
Write tests with carefully crafted docstrings using doctests.
Include generated Markdown or reST in docs.
With docsub, this can be achieved with some minimal configuration.
Just two commands to run tests and update docs:
$ pytest tests
$ docsub sync -i usage.md
usage.md¶
# Usage
<!-- docsub: begin -->
<!-- docsub: x case tests/test_usage.py:UseCase1 -->
## Use Case 1
Long description of the use case.
Usage example in doctest:
```pycon
>>> True
True
```
<!-- docsub: end -->
tests/test_usage.py¶
from unittest import TestCase
from doctestcase import doctestcase
@doctestcase()
class UseCase1(TestCase):
"""
Use Case 1
Long description of the use case.
Usage example in doctest:
>>> True
True
"""
docsubfile.py¶
Docsub configuration file declaring project-local x-tension command:
from docsub import click
from doctestcase import to_markdown
from importloc import Location
@click.group()
def x() -> None:
pass
@x.command()
@click.argument('case')
def case(case: str) -> None:
text = to_markdown(Location(case).load(), title_depth=2)
click.echo(text, nl=False)