Beginners' Guide to Effortless Doctests in Python
Doctests are essentially tests embedded in a docstring. They serve both as example use cases and test cases! A Python expression is provided along with an expected outcome, a test runner collects that and evaluates the expression.
Getting started
Let's take a look at a run-of-the-mill docstring.
def hello(name): """ Returns a greeting message saying Hello. :param name: str :return: str """ return 'Hello, ' + name
It's a normal docstring with nothing too special. Let's try to get the doctest module to test our test-less function.
$ python -m doctest -v hello.py 2 items had no tests: hello hello.hello 0 tests in 2 items. 0 passed and 0 failed. Test passed.
Well, the output says it clear that we have no tests. Now, let's add a doctest. Remember how the Python Shell or REPL works? Remember how you have three arrows indicating input? Yes. Just copy that style.
def hello(name): """ Returns a greeting message saying Hello. :param name: str :return: str >>> hello('Anam') 'Hello, Anam' """ return 'Hello, ' + name
Time to run the doctest module again.
$ python -m doctest -v hello.py Trying: hello('Anam') Expecting: 'Hello, Anam' ok 1 items had no tests: hello 1 items passed all tests: 1 tests in hello.hello 1 tests in 2 items. 1 passed and 0 failed. Test passed.
Ah, that looks more like it.
Expecting Exceptions
Let's try something more fun. Let's add a case in our function, if the provided name is just composed of digits, we refuse to greet digits!
def hello(name): """ Returns a greeting message saying Hello. :param name: str :return: str >>> hello('Anam') 'Hello, Anam' """ if name.isdigit(): raise ValueError('We do not greet numbers') return 'Hello, ' + name
Now, how do we test for exceptions? Well, remember the mantra.
Replicate your shell; success you will be showered with upon you.
So, just replicate your shell! Let's see how the shell reacts to digits.
So, in the doctest,
def hello(name): """ Returns a greeting message saying Hello. :param name: str :return: str >>> hello('Anam') 'Hello, Anam' >>> hello('121') Traceback (most recent call last): ... ValueError: We do not greet numbers """ if name.isdigit(): raise ValueError('We do not greet numbers') return 'Hello, ' + name
Just the first line and the last line of the exception. Don't confuse doctest with a complete, well-formatted and fully laid out stack trace, please! And just three dots to indicate there were more to it. So, let's run this again.
$ python -m doctest -v hello.py Trying: hello('Anam') Expecting: 'Hello, Anam' ok Trying: hello('121') Expecting: Traceback (most recent call last): ... ValueError: We do not greet numbers ok 1 items had no tests: hello 1 items passed all tests: 2 tests in hello.hello 2 tests in 2 items. 2 passed and 0 failed. Test passed.
And yes.
In Production
Of course, in production, you do need to use a test runner. Something in the category of pytest or nose. I personally prefer pytest. Let's take a look at configuring pytest for this.
Not so surprisingly, it discovered no tests in the default configuration. We need to add a simple flag to it --doctest-modules .
That seems to be working, let's try to intentionally run our tests to check if it is really working.
And that is working perfectly. To avoid putting the --doctest-modules flag all the time, consider making a tox.ini file to place your pytest configuration. More on this in the pytest documentation.