A Very Simple Doctest Example

I've only recently heard about Doctest in Python and thought it worth checking out. Simply put, Doctest will test portions of your Python code automatically. The magic of Doctest is that it's fairly easy to write!

I've been writing Python off and on for several years, but I don't consider myself an expert. I say this because I know there is so much that I don't fully understand about Python, yet. I also say it because I try to write Python tutorials that nearly anyone with a bit of Python experience can use. With that, here are a few simple examples of using Doctest that any new Python developer can use today!

Let's start with a simple function. I'm going to use the python command line to write it.

$ python3
Type "help", "copyright", "credits" or "license" for more information.
>>> def getIPByHostname(h):
...     import socket, sys
...     try:
...         hostname = str(h)
...         ip = socket.gethostbyname(hostname)
...         print(hostname + ' has an IP of ' + ip)
...     except:
...         print("Oops, something is wrong with that host")
... 
>>>

Typing the above into your Python command line will create a function. You can, of course, copy and paste from an existing python file to make things a bit easier on you. The reason we do this so we can basically run a few tests, copy and paste these tests into our doctest and run it with very few changes. Let's try it. From the same Python cli

>>> getIPByHostname("www.uc.edu")
www.uc.edu has an IP of 129.137.2.122
>>> getIPByHostname("google.com")
google.com has an IP of 172.217.4.46
>>> getIPByHostname("test")
Oops, something is wrong with that host

These are the outputs we expect when running this function. Now, we can take these outputs and use them in our doctest.

Create a new Python file, containing the function we created above. Call it whatever you want. I'll call mine doctestfun.py. The file should contain something like this:


def getIPByHostname(h):
    import socket, sys
    try:
        hostname = str(h)
        ip = socket.gethostbyname(hostname)
        print(hostname + ' has an IP of ' + ip)
    except:
        print("Oops, something is wrong with that host")

if __name__ == "__main__":
    import doctest
    doctest.testmod()

Now, using the "tests" we ran above, we just need to slightly modify the function to include our Doctest.

Copy the outputs from your Python command and paste them between two sets of """, like so:

    """Return IP address based on Hostname
    >>> getIPByHostname("www.uc.edu")
    www.uc.edu has an IP of 129.137.2.122
    >>> getIPByHostname("google.com")
    google.com has an IP of 172.217.4.46
    >>> getIPByHostname("test")
    Oops, something is wrong with that host
    """

Your complete file should look something like this :


def getIPByHostname(h):
    """Return IP address based on Hostname
    >>> getIPByHostname("www.uc.edu")
    www.uc.edu has an IP of 129.137.2.122
    >>> getIPByHostname("google.com")
    google.com has an IP of 172.217.4.46
    >>> getIPByHostname("test")
    Oops, something is wrong with that host
    """



    import socket, sys
    try:
        hostname = str(h)
        ip = socket.gethostbyname(hostname)
        print(hostname + ' has an IP of ' + ip)
    except:
        print("Oops, something is wrong with that host")

if __name__ == "__main__":
    import doctest
    doctest.testmod()

Run the script with the following command (yours may be slightly different, depending on OS)

python3 doctestfun.py -v

Passing -v will show successes and failures. Without -v, it will only show failures. Alternatively, you can force verbose every time by editing doctest.testmod() to this:

doctest.testmod(verbose=True)

The output should look something like this:

python3 doctestfun.py -v
Trying:
    getIPByHostname("www.uc.edu")
Expecting:
    www.uc.edu has an IP of 129.137.2.122
ok
Trying:
    getIPByHostname("google.com")
Expecting:
    google.com has an IP of 172.217.4.46
ok
Trying:
    getIPByHostname("test")
Expecting:
    Oops, something is wrong with that host
ok
1 items had no tests:
    __main__
1 items passed all tests:
   3 tests in __main__.getIPByHostname
3 tests in 2 items.
3 passed and 0 failed.
Test passed.

Looks good! But let's do a bit of negative testing. Let's change uc.edu's IP address to something else, anything will do.

python3 doctestfun.py -v
Trying:
    getIPByHostname("www.uc.edu")
Expecting:
    www.uc.edu has an IP of 123.123.123.123
**********************************************************************
File "doctestfun.py", line 4, in __main__.getIPByHostname
Failed example:
    getIPByHostname("www.uc.edu")
Expected:
    www.uc.edu has an IP of 123.123.123.123
Got:
    www.uc.edu has an IP of 10.23.135.100

2 passed and 1 failed.
***Test Failed*** 1 failures.

Failure == Success! So we know that our doctest is working as expected.

Let's do another test. In the same file, we can add an additional function.

def doSomeMath(num1,num2,operator):
    import math
    try:
        num1=int(num1)
        num2=int(num2)
        if operator=="+":
            return(num1 + num2)
        elif operator=="-":
            return(num1 - num2)
        else:
            return("operator not supported")
    except:
        return("invalid math operation")

Now let's add some doctests. We're smart enough now that we can emulate what we did above to just type out the tests, rather than having to generate via the commandline.

def doSomeMath(num1,num2,operator):
    """Return some simple math functions
    >>> doSomeMath(1,2,"+")
    3
    >>> doSomeMath(5,4,"-")
    1
    """
    import math
    try:
        if operator=="+":
            return(num1 + num2)
        elif operator=="-":
            return(num1 - num2)
        else:
            return("operator not supported")
    except:
        return("invalid math operation")

Run this. It will run both sets of tests, which is good enough for us. This can be broken out into separate scripts as well.

Let's add a few more tests. We can even account for Traceback exceptions.

    """Return some simple math functions
    >>> doSomeMath(1,2,"+")
    3
    >>> doSomeMath(5,4,"-")
    1
    >>> doSomeMath(4,4,"*")
    'operator not supported'
    >>> doSomeMath("a","b","+")
    'invalid math operation'
    >>> doSomeMath(a,b,"+")
    Traceback (most recent call last):
        ...
    NameError: name 'a' is not defined
    """

Run it again and you should see lots of successes.

Here is the full script


def getIPByHostname(h):
    """Return IP address based on Hostname
    >>> getIPByHostname("www.uc.edu")
    www.uc.edu has an IP of 10.23.135.100
    >>> getIPByHostname("google.com")
    google.com has an IP of 172.217.4.46
    >>> getIPByHostname("test")
    Oops, something is wrong with that host
    """

    import socket, sys
    try:
        hostname = str(h)
        ip = socket.gethostbyname(hostname)
        print(hostname + ' has an IP of ' + ip)
    except:
        print("Oops, something is wrong with that host")


def doSomeMath(num1,num2,operator):
    """Return some simple math functions
    >>> doSomeMath(1,2,"+")
    3
    >>> doSomeMath(5,4,"-")
    1
    >>> doSomeMath(4,4,"*")
    'operator not supported'
    >>> doSomeMath("a","b","+")
    'invalid math operation'
    >>> doSomeMath(a,b,"+")
    Traceback (most recent call last):
        ...
    NameError: name 'a' is not defined
    """
    import math
    try:
        num1=int(num1)
        num2=int(num2)
        if operator=="+":
            return(num1 + num2)
        elif operator=="-":
            return(num1 - num2)
        else:
            return("operator not supported")
    except:
        return("invalid math operation")

if __name__ == "__main__":
    import doctest
    doctest.testmod(verbose=True)

This may seem like "baby's first doctest", and it is! But I hope that the budding Python enthusiast finds this simple breakdown useful.

Sources: docs.python.org/3/library/doctest.html#modu.. tutorialspoint.com/testing-in-python-using-..

Code: gitlab.com/-/snippets/2079560

No Comments Yet