python doctest and unittest

Testing Python: Understanding Doctest and Unittest

Testing is crucial in the software development phase. It helps ensure easy debugging, agile code, and enhanced reusability. Performing tests that cover all use cases helps prevent a codebase from breaking — minimizing exposure to vulnerabilities. Python has two main testing frameworks that developers can use, doctest and unittest.

What is Doctest?

Doctest is an inbuilt test framework that comes bundled with Python by default. The doctest module searches for code fragments that resemble interactive Python sessions and runs those sessions to confirm they operate as shown.

Developers commonly use doctest to test documentation. The doctest module looks for sequences of prompts in a docstring, re-executes the extracted command, and compares the output with the command’s input given in the docstrings test example.

The default action when running doctests is not to show the output when a test passes. However, we can change this in the doctest runner options. In addition, doctest’s integration with the Python unittest module enables us to run doctests as standard unittest test cases.

What is Unittest?

Unittest test case runners allow more options when running tests, like reporting test statistics such as tests that passed and failed.

Unittest uses methods created in classes to manage tests. It has support for automation, setup, and shutdown code when testing. Unittest has several rich, in-built features that are unavailable in doctest, including generators and group fixture managers like setUp and tearDown. 

Since unittest follows the object-oriented method, it’s more suitable for testing class-based methods in a non-production environment. Continuous delivery tools such as Jenkins or Travis CI work better for production environments.
We’ll create a real-world example in the following sections, some code with customer information and discounts, and test it using doctest and unittest. Then we’ll analyze the tests and recommend the best ways to make further improvements.  

Using Unittest 

Let’s implement a Customer class and explore how we can test its methods using unittest.  

At the terminal command prompt, create a tests directory, then create a subdirectory named unittest. We’ll later create our Python testing scripts in the unittest directory. 

Use these commands to create the directory and subdirectory:

mkdir tests
cd tests 
mkdir unittest && cd unittest

Inside the unittest directory, create a Python script named unittest/customer.py. Then create a Customer class in that file as follows:

class Customer:
    """A sample customer class"""

    discount = 0.95

    def __init__(self, first_name, last_name, purchase):
        self.first_name = first_name
        self.last_name = last_name
        self.purchase = purchase

    @property
    def customer_mail(self):
        return f'{self.first}.{self.last}@email.com'

    @property
    def customer_fullname(self):
        return f'{self.first} {self.last}'

    def apply_discount(self):
        self.purchase = int(self.purchase * self.discount)

In the Customer class, create a constructor to instantiate our objects: first_name, last_name, and purchase. Then create these three methods: customer_mail, customer_fullname, and apply_discount

The customer_mail method returns a formatted customer’s email, customer_fullname returns the customer name, and apply_discount applies a five percent discount on the goods purchased. The @property decorator allows us to define the class methods like attributes through getters and setters.

To test the various methods we have created, we must create another Python file in the same directory and name it unittest/test_customer.py. It is a Python convention that all test files begin with a test prefix followed by the name of the file we’re testing.

Write the tests in the test_customer.py file like this:

import unittest
from customer import Customer

class TestCustomer(unittest.TestCase):

    def setUp(self):
        self.customer_1 = Customer('John', 'Brad', 5000)
        self.customer_2 = Customer('Tina', 'Smith', 3000)

    def test_customer_mail(self):
        self.assertEqual(self.customer_1.customer_mail, '[email protected]')
        self.assertEqual(self.customer_2.customer_mail, '[email protected]')

    def test_customer_fullname(self):
        self.assertEqual(self.customer_1.customer_fullname, 'John Brad')
        self.assertEqual(self.customer_2.customer_fullname, 'Tina Smith')

    def test_apply_discount(self):
        self.customer_1.apply_discount()
        self.customer_2.apply_discount()

        self.assertEqual(self.customer_1.purchase, 4750)
        self.assertEqual(self.customer_2.purchase, 2850)


if __name__ == '__main__':
    unittest.main()

Now, we import unittest from the Python package, then import the Customer class we created in the customer.py file. In the TestCustomer class, we pass the TestCase from unittest as it allows us to check specific responses for every set of inputs. We then create three individual tests with methods starting with the prefix test. The test prefix shows the test runner which methods to test. 

The setUp method is the first to execute when our tests run, and it’s where we define the customer instances. We use the assert method, assertEqual, to check equality for expected results in each test method.

For example, test_customer_mail checks whether the customer’s email corresponds to the one provided using the logic defined in the unittest/customer.py file and the attributes passed for that particular object. The same applies to the test_customer_fullname method.

In the last method, we apply the discount to the initial purchase and check the value passed. To learn more about various assert methods used in unittest, check out Python’s documentation

The final block shows a simple command-line feature that enables us to run our tests. If we execute our tests in the terminal using python3 test_customer.py, the output is as follows:

...
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

All our tests passed. If we try to alter the last method in our test and change the discount of the second customer to “2750”, our code throws an error as shown below:

F..
======================================================================
FAIL: test_apply_discount (__main__.TestCustomer)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_customer.py", line 23, in test_apply_discount
    self.assertEqual(self.customer_2.purchase, 2750)
AssertionError: 2850 != 2750

----------------------------------------------------------------------
Ran 3 tests in 0.000s

FAILED (failures=1)

This testing helps to debug quickly and shows us where in the code we went wrong. By using unittest and writing tests that cover all use cases, we can improve our program’s readability, functionality, and logic flow.

Using Doctest

Using doctest is more straightforward than unittest, and has fewer steps. Although doctest is easy to use, we should use it wisely as there are several caveats. 

Let’s start our demonstration using the same example as before.

In the tests directory we created, create a new directory, name it tests/doctest and create a doctest_customer.py file in it using these commands:

mkdir doctest
cd doctest && touch doctest_customer.py

We now have two test directories, unittest and doctest. The unittest directory is where we created our unittest examples earlier. 
Inside doctest/doctest_customer.py, create the Customer class as follows:

class Customer:
    """
    A sample customer class
    """
    discount = 0.95

    def __init__(self, first, last, purchase):
        self.first = first
        self.last = last
        self.purchase = purchase

    def customer_mail(self):
        return f'{self.first}.{self.last}@email.com'

    def customer_fullname(self):
        return f'{self.first} {self.last}'

    def apply_discount(self):
        self.purchase = int(self.purchase * self.discount)
        return self.purchase

In the terminal, type python3 to get into the Python shell, then perform this operation:

Python 3.8.10 (default, Nov 26 2021, 20:14:08)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from customer import Customer
>>> customer_1 = Customer("John", "Brad", 5000)
>>> customer_2 = Customer("Tina", "Smith", 3000)
>>> customer_1.customer_mail()
'[email protected]'
>>> customer_2.customer_mail()
'[email protected]'
>>> customer_1.customer_fullname()
'John Brad'
>>> customer_2.customer_fullname()
'Tina Smith'
>>> customer_1.apply_discount()
4750
>>> customer_2.apply_discount()
2850
>>>

In the snippet above, we call some of the methods in our class from the command line. 

To run the doctest, place the output above into the class docstring like this:

class Customer:
    """
    A sample customer class

    >>> customer_1 = Customer("John", "Brad", 5000)
    >>> customer_2 = Customer("Tina", "Smith", 3000)
    >>> customer_1.customer_mail()
    '[email protected]'
    >>> customer_2.customer_mail()
    '[email protected]'
    >>> customer_1.customer_fullname()
    'John Brad'
    >>> customer_2.customer_fullname()
    'Tina Smith'
    >>> customer_1.apply_discount()
    4750
    >>> customer_2.apply_discount()
    2850
    """
    discount = 0.95

    def __init__(self, first, last, purchase):
        self.first = first
        self.last = last
        self.purchase = purchase

    def customer_mail(self):
        return f'{self.first}.{self.last}@email.com'

    def customer_fullname(self):
        return f'{self.first} {self.last}'

    def apply_discount(self):
        self.purchase = int(self.purchase * self.discount)
        return self.purchase

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

To be able to execute our tests, we import doctest below the file, and run our tests by executing python3 doctest_customer.py -v in the terminal. 

The output is:

Trying:
    customer_1 = Customer("John", "Brad", 5000)
Expecting nothing
ok
Trying:
    customer_2 = Customer("Tina", "Smith", 3000)
Expecting nothing
ok
Trying:
    customer_1.customer_mail()
Expecting:
    '[email protected]'
ok
Trying:
    customer_2.customer_mail()
Expecting:
    '[email protected]'
ok
Trying:
    customer_1.customer_fullname()
Expecting:
    'John Brad'
ok
Trying:
    customer_2.customer_fullname()
Expecting:
    'Tina Smith'
ok
Trying:
    customer_1.apply_discount()
Expecting:
    4750
ok
Trying:
    customer_2.apply_discount()
Expecting:
    2850
ok
5 items had no tests:
    __main__
    __main__.Customer.__init__
    __main__.Customer.apply_discount
    __main__.Customer.customer_fullname
    __main__.Customer.customer_mail
1 items passed all tests:
   8 tests in __main__.Customer
8 tests in 6 items.
8 passed and 0 failed.
Test passed.

All the tests passed. This demonstration makes it safe to say that doctest is more suitable for writing executable documentation for a package while unittest is better suited for testing documentation, and we can use this feature when reducing bugs in software documentation. For more information about doctest, see Python’s documentation.

Next Steps

Testing is a critical software development process since it ensures the code is clean and works as expected. Choosing the right way to test your code before executing tests is crucial. 

Performing tests using the unittest framework allows us to test our code in an object-oriented approach. It supports many features such as test automation, setup, and code test teardown. Doctest, in contrast, is more suitable for documentation since we can insert it in the code’s docstrings.

This blog post was created as part of the Mattermost Community Writing Program and is published under the CC BY-NC-SA 4.0 license. To learn more about the Mattermost Community Writing Program, check this out.

Read more about:

Python QA Testing

Okoth Pius is a Technical Author at ContentLab. Developing software and contributing to open-source projects is always in his daily objectives, trying to make an impact on the software community and creating a medium for others to learn. He focuses mainly on DevOps and software development.