Unit test example

Suppose we want to write a function to convert a temperature between the units Fahrenheit, Celsius and Kelvin (identified by the characters 'F', 'C' and 'K' respectively). The six formulae involved are not difficult to code, but we might wish to handle gracefully a couple of conditions that could arise in the use of this function: a physically unrealisable temperature ($< 0\;\mathrm{K}$) or a unit other than 'F', 'C' or 'K'.

Our function will first convert to Kelvin and then to the units requested; if the from-units and the to-units are the same for some reason, we want to return the original value unchanged. The function convert_temperature is defined in the file temperature_utils.py below.

# temperature_utils.py

def convert_temperature(value, from_unit, to_unit):
    """ Convert and return the temperature value from from_unit to to_unit. """

    # Dictionary of conversion functions from different units *to* K
    toK = {'K': lambda val: val,
           'C': lambda val: val + 273.15,
           'F': lambda val: (val + 459.67)*5/9,
          }
    # Dictionary of conversion functions *from* K to different units
    fromK = {'K': lambda val: val,
             'C': lambda val: val - 273.15,
             'F': lambda val: val*9/5 - 459.67,
            }

    # First convert the temperature from from_unit to K
    try:
        T = toK[from_unit](value)
    except KeyError:
        raise ValueError('Unrecognised temperature unit: {}'.format(from_unit))

    if T < 0:
        raise ValueError('Invalid temperature: {} {} is less than 0 K'
                                .format(value, from_unit))

    if from_unit == to_unit:
       # No conversion needed!
        return value

    # Now convert it from K to to_unit and return its value
    try:
        return fromK[to_unit](T)
    except KeyError:
        raise ValueError('Unrecognised temperature unit: {}'.format(to_unit))

To use the unittest module to conduct unit tests on the convert_temperature, we write a new Python script defining a class, TestTemperatureConversion, derived from the base unittest.TestCase class. This class defines methods which act as tests of the convert_temperature function. These test methods should call one of the base class's assertion functions to validate that the return value of convert_temperature is as expected. For example,

self.assertEqual(<returned value>, <expected value>)

returns True if the two values are exactly equal and False otherwise. Other assertion functions exist to check that a specific exception is raised (for example, by invalid arguments) or that a returned value is True, False, None, etc. The unit test code for our convert_temperature function is below.

from temperature_utils import convert_temperature
import unittest

class TestTemperatureConversion(unittest.TestCase):

    def test_invalid(self):
        """
        There's no such temperature as -280 C, so convert_temperature should
        raise a ValueError.
        """
        self.assertRaises(ValueError, convert_temperature, -280, 'C', 'F')

    def test_valid(self):
        """ A series of valid temperature conversions to test. """

        test_cases = [((273.16, 'K',), (0.01, 'C')),
                      ((-40, 'C'), (-40, 'F')),
                      ((450, 'F'), (505.3722222222222, 'K'))]

        for test_case in test_cases:
            ((from_val, from_unit), (to_val, to_unit)) = test_case
            result = convert_temperature(from_val, from_unit, to_unit)
            self.assertAlmostEqual(to_val, result)

    def test_no_conversion(self):
        """
        Ensure that if the from-units and to-units are the same the
        temperature is returned exactly as it was passed and not converted
        to and from Kelvin, which may cause loss of precision.

        """
        T = 56.67
        result = convert_temperature(T, 'C', 'C')
        self.assertEqual(result, T)

    def test_bad_units(self):
        """ Check that ValueError is raised if invalid units are passed. """
        self.assertRaises(ValueError, convert_temperature, 0, 'C', 'R')
        self.assertRaises(ValueError, convert_temperature, 0, 'N', 'K')

unittest.main()
  • assertRaises verifies that a specified exception is raised by the method convert_temperature. The necessary arguments to this method are passed after the method object itself.

  • We need assertAlmostEqual here because the floating point arithmetic is likely to cause a loss of precision due to rounding errors.

  • We use assertEqual here to ensure that the temperature value is returned as it was passed and not converted to and from Kelvin.

Running this script shows that our function passes its unit tests:

$ python eg9-temperature-conversion-unittest.py
...
----------------------------------------------------------------------
Ran 4 tests in 0.000s

OK