Skip to content

Commit 24afc09

Browse files
authored
feat: run doctest as part of running tests, which collects doctests from both the package’s doc strings and the package documentation (#637)
1 parent 85ee398 commit 24afc09

5 files changed

Lines changed: 65 additions & 22 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ repos:
162162
hooks:
163163
- id: pytest
164164
name: Run unit tests
165-
entry: pytest -c pyproject.toml --cov-config pyproject.toml
165+
entry: pytest -c pyproject.toml --cov-config pyproject.toml src/package/ tests/ docs/
166166
language: python
167167
verbose: true
168168
always_run: true

README.md

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ A number of git hooks are invoked before and after a commit, and before push. Th
4040

4141
### Unit testing
4242

43-
Comprehensive unit testing is enabled using [pytest](https://pytest.org/) combined with [Hypothesis](https://hypothesis.works/) (to generate test payloads and strategies), and test code and branch coverage is measured using [coverage](https://github.com/nedbat/coveragepy) (see [below](#testing)).
43+
Comprehensive unit testing is enabled using [pytest](https://pytest.org/) combined with [doctest](https://docs.python.org/3/library/doctest.html) and [Hypothesis](https://hypothesis.works/) (to support [property-based testing](https://en.wikipedia.org/wiki/Software_testing#Property_testing)), and both code and branch coverage are measured using [coverage](https://github.com/nedbat/coveragepy) (see [below](#testing)).
4444

4545
### Documentation
4646

@@ -165,25 +165,28 @@ As mentioned above, this repository is set up to use [pytest](https://pytest.org
165165
```bash
166166
make test
167167
```
168-
which runs all tests in both your local Python virtual environment. For more options, see the [pytest command-line flags](https://docs.pytest.org/en/6.2.x/reference.html#command-line-flags). Also note that pytest includes [doctest](https://docs.python.org/3/library/doctest.html), which means that module and function [docstrings](https://www.python.org/dev/peps/pep-0257/#what-is-a-docstring) may contain test code that executes as part of the unit tests.
168+
which runs all tests in both your local Python virtual environment. For more options, see the [pytest command-line flags](https://docs.pytest.org/en/7.4.x/reference/reference.html#command-line-flags). Also note that pytest includes [doctest](https://docs.python.org/3/library/doctest.html), which means that module and function [docstrings](https://www.python.org/dev/peps/pep-0257/#what-is-a-docstring), as well as the documentation, may contain test code that executes as part of the unit tests.
169169

170-
Test code and branch coverage is already tracked using [coverage](https://github.com/nedbat/coveragepy) and the [pytest-cov](https://github.com/pytest-dev/pytest-cov) plugin for pytest, and it measures how much code in the `src/package/` folder is covered by tests:
170+
Both statement and branch coverage are being tracked using [coverage](https://github.com/nedbat/coveragepy) and the [pytest-cov](https://github.com/pytest-dev/pytest-cov) plugin for pytest, and it measures how much code in the `src/package/` folder is covered by tests:
171171
```
172172
Run unit tests...........................................................Passed
173173
- hook id: pytest
174-
- duration: 0.48s
174+
- duration: 0.6s
175175
176176
============================= test session starts ==============================
177-
platform darwin -- Python 3.10.2, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 -- /.../python-package-template/.venv/bin/python3.10
177+
platform darwin -- Python 3.11.7, pytest-7.4.4, pluggy-1.3.0 -- /path/to/python-package-template/.venv/bin/python
178178
cachedir: .pytest_cache
179-
hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase('/.../python-package-template/.hypothesis/examples')
180-
rootdir: /.../python-package-template, configfile: pyproject.toml, testpaths: tests
181-
plugins: hypothesis-6.41.0, cov-3.0.0
182-
collected 1 item
179+
hypothesis profile 'default-with-verbose-verbosity-with-explain-phase' -> max_examples=500, verbosity=Verbosity.verbose, phases=(Phase.explicit, Phase.reuse, Phase.generate, Phase.target, Phase.shrink, Phase.explain), database=DirectoryBasedExampleDatabase('/path/to/python-package-template/.hypothesis/examples')
180+
rootdir: /path/to/python-package-template
181+
configfile: pyproject.toml
182+
plugins: custom-exit-code-0.3.0, cov-4.1.0, doctestplus-1.1.0, hypothesis-6.90.0, env-1.1.1
183+
collected 3 items
183184
184-
tests/test_something.py::test_something PASSED [100%]
185+
src/package/something.py::package.something.Something.do_something PASSED [ 33%]
186+
tests/test_something.py::test_something PASSED [ 66%]
187+
docs/source/index.rst::index.rst PASSED [100%]
185188
186-
---------- coverage: platform darwin, python 3.10.2-final-0 ----------
189+
---------- coverage: platform darwin, python 3.11.7-final-0 ----------
187190
Name Stmts Miss Branch BrPart Cover Missing
188191
----------------------------------------------------------------------
189192
src/package/__init__.py 1 0 0 0 100%
@@ -197,20 +200,20 @@ Required test coverage of 100.0% reached. Total coverage: 100.00%
197200
tests/test_something.py::test_something:
198201
199202
- during reuse phase (0.00 seconds):
200-
- Typical runtimes: ~ 1ms, ~ 28% in data generation
203+
- Typical runtimes: < 1ms, of which < 1ms in data generation
201204
- 1 passing examples, 0 failing examples, 0 invalid examples
202205
203206
- during generate phase (0.00 seconds):
204-
- Typical runtimes: < 1ms, ~ 43% in data generation
207+
- Typical runtimes: < 1ms, of which < 1ms in data generation
205208
- 1 passing examples, 0 failing examples, 0 invalid examples
206209
207210
- Stopped because nothing left to do
208211
209-
============================== 1 passed in 0.16s ===============================
212+
============================== 3 passed in 0.05s ===============================
210213
```
211-
Note that code that’s not covered by tests is listed under the `Missing` column, and branches not taken too. The net effect of enforcing 100% code and branch coverage is that every new major and minor feature, every code change, and every fix are being tested (keeping in mind that high _coverage_ does not necessarily imply comprehensive _test data_).
214+
Note that code that’s not covered by tests is listed under the `Missing` column, and branches not taken too. The net effect of enforcing 100% code and branch coverage is that every new major and minor feature, every code change, and every fix are being tested (keeping in mind that high _coverage_ does not imply comprehensive, meaningful _test data_).
212215

213-
Hypothesis is a package that implements [property based testing](https://en.wikipedia.org/wiki/QuickCheck) and that provides payload generation for your tests based on strategy descriptions ([more](https://hypothesis.works/#what-is-hypothesis)). Using its [pytest plugin](https://hypothesis.readthedocs.io/en/latest/details.html#the-hypothesis-pytest-plugin) Hypothesis is ready to be used for this package.
216+
Hypothesis is a package that implements [property based testing](https://en.wikipedia.org/wiki/Software_testing#Property_testing) and that provides payload generation for your tests based on strategy descriptions ([more](https://hypothesis.works/#what-is-hypothesis)). Using its [pytest plugin](https://hypothesis.readthedocs.io/en/latest/details.html#the-hypothesis-pytest-plugin) Hypothesis is ready to be used for this package.
214217

215218
## Generating documentation
216219

docs/source/index.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ Package package
1616
Something
1717
=========
1818

19+
The ``Something`` module contains a useful class which allows you to do something
20+
like the following:
21+
22+
.. code: pycon
23+
24+
>>> from package import something
25+
>>> s = something.Something()
26+
>>> s.do_something()
27+
True
28+
>>> s.do_something(False) # doctest: +SKIP
29+
False # This value would fail the test.
30+
1931
.. automodule:: package
2032
:members:
2133

pyproject.toml

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ test = [
6363
"pytest >=7.2.0,<8.0.0",
6464
"pytest-custom_exit_code ==0.3.0",
6565
"pytest-cov ==4.1.0",
66+
"pytest-doctestplus ==1.1.0",
6667
"pytest-env ==1.1.1",
6768
]
6869

@@ -207,13 +208,28 @@ max-line-length = 120
207208
# https://docs.pytest.org/en/latest/reference/customize.html#configuration-file-formats
208209
# https://docs.pytest.org/en/latest/reference/reference.html#configuration-options
209210
# https://docs.pytest.org/en/latest/reference/reference.html#command-line-flags
211+
#
212+
# To integrate Hypothesis into pytest and coverage, we use its native plugin:
213+
# https://hypothesis.readthedocs.io/en/latest/details.html#the-hypothesis-pytest-plugin
214+
#
215+
# To discover tests in documentation, we use doctest and the doctest-plus plugin which
216+
# adds multiple useful options to control tests in documentation. More details at:
217+
# https://docs.python.org/3/library/doctest.html
218+
# https://github.com/scientific-python/pytest-doctestplus
219+
#
220+
# To avoid failing pytest when no tests were dicovered, we need an extra plugin:
221+
# https://docs.pytest.org/en/latest/reference/exit-codes.html
222+
# https://github.com/yashtodi94/pytest-custom_exit_code
210223
[tool.pytest.ini_options]
211224
minversion = "7.0"
212-
addopts = "-vv --doctest-modules --tb native --hypothesis-show-statistics --hypothesis-explain --hypothesis-verbosity verbose -ra --cov package" # Consider adding --pdb
225+
addopts = """-vv -ra --tb native \
226+
--hypothesis-show-statistics --hypothesis-explain --hypothesis-verbosity verbose \
227+
--doctest-modules --doctest-continue-on-failure --doctest-glob '*.rst' --doctest-plus \
228+
--suppress-no-test-exit-code \
229+
--cov package \
230+
""" # Consider adding --pdb
231+
# https://docs.python.org/3/library/doctest.html#option-flags
213232
doctest_optionflags = "IGNORE_EXCEPTION_DETAIL"
214-
testpaths = [
215-
"tests",
216-
]
217233
env = [
218234
"PYTHONDEVMODE=1", # https://docs.python.org/3/library/devmode.html
219235
]

src/package/something.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,17 @@ class Something:
66

77
@staticmethod
88
def do_something(value: bool = False) -> bool:
9-
"""Return true, always."""
9+
"""Return true, always.
10+
11+
Test this function in your local terminal, too, for example:
12+
13+
.. code: pycon
14+
15+
>>> s = Something()
16+
>>> s.do_something(False)
17+
True
18+
>>> s.do_something(value=True)
19+
True
20+
21+
"""
1022
return value or True

0 commit comments

Comments
 (0)