Python unittest subtest

Summary: in this tutorial, you’ll learn how to define parameterized tests using unittest‘s subTest() context manager.

Introduction to the unittest subtest context manager

First, create a new module called pricing.py and define a calculate() function as follows:

def calculate(price, tax=0, discount=0):
    return round((price - discount) * (1+tax), 2)Code language: Python (python)

The calculate() function calculates the net price from the price, tax, and discount.

Second, create the test_pricing.py test module to test the calculate() function:

import unittest

from pricing import calculate


class TestPricing(unittest.TestCase):
    def test_calculate(self):
        passCode language: Python (python)

To test the calculate() function, you need to come up with multiple test cases, for example:

  • Has price with no tax and no discount
  • Has price with tax but no discount
  • Has price with no tax and discount
  • Has price with tax and discount

To cover these cases, you need to have various test methods. Or you can define a single test method and supply test data from a list of cases. For example:

import unittest

from pricing import calculate


class TestPricing(unittest.TestCase):
    def test_calculate(self):
        items = (
            {'case': 'No tax, no discount', 'price': 10, 'tax': 0, 'discount': 0, 'net_price': 10},
            {'case': 'Has tax, no discount', 'price': 10, 'tax': 0.1, 'discount': 0, 'net_price': 10},
            {'case': 'No tax, has discount', 'price': 10, 'tax': 0, 'discount': 1, 'net_price': 10},
            {'case': 'Has tax, has discount', 'price': 10, 'tax': 0.1, 'discount': 1, 'net_price': 9.9},
        )

        for item in items:
            with self.subTest(item['case']):
                net_price = calculate(
                    item['price'],
                    item['tax'],
                    item['discount']
                )
                self.assertEqual(
                    net_price,
                    item['net_price']
                )Code language: Python (python)

Run the test:

python -m unittest test_pricing -vCode language: Python (python)

Output:

test_calculate (test_pricing.TestPricing) ... FAIL

======================================================================     
FAIL: test_calculate (test_pricing.TestPricing)
----------------------------------------------------------------------     
Traceback (most recent call last):
  File "D:\python-unit-testing\test_pricing.py", line 26, in test_calculate
    self.assertEqual(
AssertionError: 11.0 != 10

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (failures=1)Code language: plaintext (plaintext)

The problem with this approach is that the test stops after the first failure. To resolve this, the unittest provides you with the subTest() context manager. For example:

import unittest

from pricing import calculate


class TestPricing(unittest.TestCase):
    def test_calculate(self):
        items = (
            {'case': 'No tax, no discount', 'price': 10, 'tax': 0, 'discount': 0, 'net_price': 10},
            {'case': 'Has tax, no discount', 'price': 10, 'tax': 0.1, 'discount': 0, 'net_price': 10},
            {'case': 'No tax, has discount', 'price': 10, 'tax': 0, 'discount': 1, 'net_price': 10},
            {'case': 'Has tax, has discount', 'price': 10, 'tax': 0.1, 'discount': 1, 'net_price': 9.9},
        )

        for item in items:
            with self.subTest(item['case']):
                net_price = calculate(
                    item['price'],
                    item['tax'],
                    item['discount']
                )
                self.assertEqual(
                    net_price,
                    item['net_price']
                )Code language: Python (python)

Run the test:

python -m unittest test_pricing -vCode language: Python (python)

Output:

test_calculate (test_pricing.TestPricing) ... 
======================================================================     
FAIL: test_calculate (test_pricing.TestPricing) [Has tax, no discount]     
----------------------------------------------------------------------     
Traceback (most recent call last):
  File "D:\python-unit-testing\test_pricing.py", line 26, in test_calculate
    self.assertEqual(
AssertionError: 11.0 != 10

======================================================================     
FAIL: test_calculate (test_pricing.TestPricing) [No tax, has discount]     
----------------------------------------------------------------------     
Traceback (most recent call last):
  File "D:\python-unit-testing\test_pricing.py", line 26, in test_calculate
    self.assertEqual(
AssertionError: 9 != 10

----------------------------------------------------------------------     
Ran 1 test in 0.001s

FAILED (failures=2)Code language: plaintext (plaintext)

By using the subTest() context manager, the test didn’t stop after the first failure. Also, it shows a very detailed message after each failure so that you can examine the case.

The subTest() context manager syntax

The following shows the subTest() context manager syntax:

 def subTest(self, msg=_subtest_msg_sentinel, **params):Code language: Python (python)

The subTest() returns a context manager. The optional message parameter identifies the closed block of the subtest returned by the context manager.

If a failure occurs, it’ll mark the test case as failed. However, it resumes execution at the end of the enclosed block, allowing further test code to be executed.

Summary

  • Use the unittest subTest() context manager to parameterize tests.
Did you find this tutorial helpful ?