Automatic testing of code examples in README is an area that is often neglected. I’d like to share some experience I got when was integrating doctests from README.rst
of paka.cmark into existing unittest
-based test discovery.
Let’s say there are following files:
somelib-project/
README.rst
somelib/
...
tests/
test_readme.py
To run tests from tests/
dir following command is executed from inside somelib-project/
dir:
$ python3 -m unittest discover --start-directory tests/
Inside test_readme.py
file doctest
examples are parsed from README.rst
and then are exposed via load_tests
protocol:
import doctest
import unittest
def load_tests(loader, tests, pattern):
suite = doctest.DocFileSuite("../README.rst")
tests.addTests(suite)
return tests
This might be enough in general. Now for the specifics. Tests and code of paka.cmark
have to support both Python 2.7 and 3.5 with single source. As paka.cmark.to_html
accepts and returns unicode
in 2.7 and str
in 3, use of repr
in doctests will fail for one version of language, and succeed for other. That is, not using “u” string prefix will cause failure under 2.7:
>>> cmark.to_html(u"Hello,\n*World*!")
'<p>Hello, <em>World</em>!</p>\n'
Failed example:
cmark.to_html(u"Hello,\n*World*!")
Expected:
'<p>Hello, <em>World</em>!</p>\n'
Got:
u'<p>Hello, <em>World</em>!</p>\n'
And using “u” string prefix will result in fail under 3.5 (but tests will pass for 2.7):
>>> cmark.to_html(u"Hello,\n*World*!")
u'<p>Hello, <em>World</em>!</p>\n'
Failed example:
cmark.to_html(u"Hello,\n*World*!")
Expected:
u'<p>Hello, <em>World</em>!</p>\n'
Got:
'<p>Hello, <em>World</em>!</p>\n'
I use a “hack”—wrap each call with print
:
>>> print(cmark.to_html(u"Hello,\n*World*!"))
<p>Hello, <em>World</em>!</p>
But just wrapping them is not enough, as resulting HTML has newline at the end:
Failed example:
print(cmark.to_html(u"Hello,\n*World*!"))
Expected:
<p>Hello, <em>World</em>!</p>
Got:
<p>Hello, <em>World</em>!</p>
<BLANKLINE>
With doctest directive that problem is solved:
>>> print(cmark.to_html(u"Hello,\n*World*!")) # doctest: +NORMALIZE_WHITESPACE
<p>Hello, <em>World</em>!</p>
If you don’t want to add such directives to each example individually, you can use optionflags
argument of doctest.DocFileSuite
to pass doctest.NORMALIZE_WHITESPACE
(and thus use that directive for all examples):
import doctest
import unittest
def load_tests(loader, tests, pattern):
suite = doctest.DocFileSuite(
"../README.rst", optionflags=doctest.NORMALIZE_WHITESPACE)
tests.addTests(suite)
return tests
This blog is about things I encounter while doing web and non-web software development.