DEV Community 👩‍💻👨‍💻

chaitdwivedi
chaitdwivedi

Posted on • Updated on

Documenting your way to better tests in Python

Writing and Reading Code

Writing code is hard, reading code is harder

I participate in a lot of code reviews and one thing I've realized is - developers spend most of their time coding a solution, not enough time explaining it.

There are two ways you could explain how your code works:

  1. Write simple code that is self-explanatory
  2. Write "good" documentation

Value of Good Documentation

In this post, I am going to make a case for how writing good documentation can improve the quality of your code and should be done along with writing self-explanatory code.

Let me illustrate with an example.

I've defined my_function below, that computes some "arbitrary" logic:

def my_function(input_string, input_list=None):
    head = "Start:"
    tail = "" if not input_list else " ".join(input_list)
    output = f"{input_string} {tail}"
    if not input_string.startswith(head):
        output = f"{head} {input_string} {tail}"

    return output.strip()
Enter fullscreen mode Exit fullscreen mode

In a real-world problem, this piece of code might be much more complex.

As a reader, you could read this code and understand what it is doing. However, I can make the reader's job easier by using docstring.

Docstrings: Make code easy to read

Docstring holds immense value when developing code in a large organization and/or in a collaborative community.

It lets any developer understand what's happening in the function/module without them having to read through the entire codebase.

It can also be used with tools like sphinxdoc to generate beautiful documentation for your project.

def my_function(input_string, input_list=None):
    """Sanitize input string and append extra text if required

    The function checks if input_string starts with 'Start:' 
    if not, it will add the string to the input_string 

    It also converts input_list to a string using join 
    and appends to the input_string
    """
    head = "Start:"
    tail = "" if not input_list else " ".join(input_list)
    output = f"{input_string} {tail}"
    if not input_string.startswith(head):
        output = f"{head} {input_string} {tail}"

    return output.strip()
Enter fullscreen mode Exit fullscreen mode

Docstring in the above code explains what the code is doing, but it still feels like a repetition of what is written in the code block.

How do we improve this?
Using doctest!

Doctest: Read, Test and Document better

doctest allows you to not only test interactive Python examples but also makes sure your documentation is up to date.

Let us take a look at improved docstring for the same function using doctest

def my_function(input_string, input_list=None):
    """Sanitize input string and append extra text if required

    >>> my_function('hi')
    'Start: hi'

    >>> my_function('Start: some string')
    'Start: some string'

    >>> my_function('hi', ['other', 'item'])
    'Start: hi other item'

    :param input_string: string to sanitize
    :type input_string: str
    :param input_list: extra items to append, defaults to None
    :type input_list: list, optional
    :return: sanitized string
    :rtype: str
    """
    head = "Start:"
    tail = "" if not input_list else " ".join(input_list)
    output = f"{input_string} {tail}"
    if not input_string.startswith(head):
        output = f"{head} {input_string} {tail}"

    return output.strip()
Enter fullscreen mode Exit fullscreen mode

Here, I have defined some input-output examples for the given function which illustrate what the function is doing. For instance:

my_function('hi', ['other', 'item'])
Enter fullscreen mode Exit fullscreen mode

should return:

'Start: hi other item'
Enter fullscreen mode Exit fullscreen mode

The above documentation tells developers/readers the following:

  • What the function does
  • Parameters and their types
  • Return value and its type
  • Expected behavior - describes input/output examples

Conclusion

Generating documentation

I used sphinxdoc to generate documentation for the above function:

image

Practicing TDD

Writing documentation in Python also allows me to follow Test Driven Development, where I first define the behavior in docstring then write the code.

doctest can be run by any testing framework like unittest or pytest

$ pytest --doctest-modules -vv top.py
=================== test session starts ======================
platform darwin -- Python 3.8.2, pytest-6.2.4 
-- /projects/virtualenvs/dev-to/bin/python3
cachedir: .pytest_cache
rootdir: /Users/chaitanyadwivedi/projects/dev-to
collected 1 item                                                                                                                                           

top.py::top.my_function PASSED                          [100%]

==================== 1 passed in 0.05s =======================
Enter fullscreen mode Exit fullscreen mode

Top comments (0)

🌚 Life is too short to browse without dark mode