Python tutorials > Testing > Doctests > How to write doctests?

How to write doctests?

Doctests are a simple way to test your Python code directly within your docstrings. They allow you to embed test cases within the documentation itself, making the code more readable and easier to verify.

Basic Doctest Example

This example demonstrates a basic doctest. The add function's docstring contains two test cases. Each test case consists of a Python expression preceded by >>>, followed by the expected output on the next line. When doctest.testmod() is run, it executes these test cases and compares the actual output with the expected output. If there's a mismatch, the test fails. The if __name__ == '__main__': block ensures the doctests are only run when the script is executed directly.

def add(a, b):
    """Return the sum of a and b.

    >>> add(2, 3)
    5
    >>> add(-1, 1)
    0
    """
    return a + b

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

Running Doctests

To run the doctests, save the code example above as a Python file (e.g., my_module.py). Then, execute the file from your terminal using the command python my_module.py -v. The -v flag (verbose) provides detailed output, showing each test case and whether it passed or failed. Without the -v flag, you'll only see output if a test fails.

# Save the code above as 'my_module.py'
# Run in the terminal:
# python my_module.py -v

Concepts Behind the Snippet

The core idea behind doctests is to embed tests directly into the documentation. This has several benefits:

  1. Documentation as Tests: Your documentation is automatically tested, ensuring that the examples provided are accurate.
  2. Improved Readability: The code becomes more understandable because it includes working examples.
  3. Simplified Testing: Doctests are easy to write and run, making it simple to add tests to your code.

Doctests rely on Python's doctest module, which parses the docstrings, identifies the test cases, executes the code, and compares the output.

Real-Life Use Case

Consider a function that formats names. Instead of writing separate unit tests, you can include doctests directly in the function's docstring. This makes it clear how the function is intended to be used and provides immediate verification that it works as expected.

def format_name(first_name, last_name):
    """Formats a name into 'Last Name, First Name' format.

    >>> format_name('John', 'Doe')
    'Doe, John'
    >>> format_name('Alice', 'Smith')
    'Smith, Alice'
    """
    return f'{last_name}, {first_name}'

Best Practices

  • Keep it Simple: Doctests are best suited for testing small, self-contained pieces of code. Avoid complex setups or external dependencies.
  • Write Clear Expectations: Ensure that the expected output is unambiguous and matches the actual output exactly (including whitespace).
  • Use the -v Flag: When running doctests, use the -v flag to get detailed output and verify that all tests are passing.
  • Focus on Core Functionality: Doctests should focus on the core functionality of the code being documented.

Interview Tip

When asked about testing strategies, mentioning doctests demonstrates a good understanding of simple testing methodologies that integrate with documentation. It shows you care about code quality and documentation. You can explain the benefits of doctests, like ease of use and integrated documentation/testing.

When to use them

Use doctests when you want to:

  • Quickly test simple functions or methods.
  • Provide clear examples of how to use your code in the documentation.
  • Ensure that your documentation is up-to-date and accurate.

Avoid doctests for:

  • Complex testing scenarios.
  • Code that relies on external dependencies or requires significant setup.
  • Performance-critical tests.

Memory Footprint

Doctests have a minimal memory footprint. Each test case is executed independently, and the results are compared directly. The doctest module itself has a small overhead.

Alternatives

Alternatives to doctests include:

  • unittest: A more comprehensive testing framework for complex tests.
  • pytest: Another popular testing framework with a simpler syntax and extensive plugin ecosystem.
  • nose2: An alternative to unittest with improved discovery and plugin support.

Pros

  • Easy to write and run.
  • Integrates testing with documentation.
  • Provides immediate feedback on code correctness.

Cons

  • Not suitable for complex tests.
  • Can be difficult to maintain for large codebases.
  • Whitespace sensitive.

FAQ

  • How do I handle exceptions in doctests?

    You can test for exceptions using the doctest.EXPECT_EXCEPTION flag or by including the expected exception type in the output:

    
    >>> 1 / 0 # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    ZeroDivisionError: division by zero
    
    

    Or you can directly expect the exception to occur:

    
    >>> raise ValueError("Invalid value")  # doctest: +EXPECT_EXCEPTION
    Traceback (most recent call last):
    ...ValueError: Invalid value
    
    
  • Can I ignore whitespace differences in doctests?

    Yes, you can use the doctest.NORMALIZE_WHITESPACE flag when running the tests. This flag ignores differences in whitespace when comparing the actual output with the expected output.

    
    import doctest
    import my_module  # Replace with your module name
    
    doctor = doctest.DocTestFinder().find(my_module)[0]
    doctor.options[doctest.NORMALIZE_WHITESPACE] = True
    
    if __name__ == '__main__':
        doctest.testmod(verbose=True)
    
    
  • How do I test functions that produce random output?

    Testing functions that produce random output can be tricky. You can use the doctest.ELLIPSIS flag to ignore parts of the output that vary. Alternatively, you can mock the random number generator to produce predictable results.

    
    >>> import random
    >>> random.random()  # doctest: +ELLIPSIS
    0.123...