Unit testing: converting between temperature units

Suppose we want to write a function to convert a temperature between the units degrees Fahrenheit, degrees Celsius and kelvins (identified by the characters 'F', 'C' and 'K', respectively). The six formulas 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 unrealizable temperature ($< 0\;\mathrm{K}$) or a unit other than 'F', 'C' or 'K'.

Our function will first convert to kelvins 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.

# temperature_utils.py

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

    # A 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,
          }
    # A 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('Unrecognized 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('Unrecognized 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 that 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 (e.g. by invalid arguments) or that a returned value is True, False, None, and so on. The unit test code for our convert_temperature function is here.

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 kelvins, which may cause loss of precision.

        """
        T = 56.67
        result = convert_temperature(T, 'C', 'C')
        self.assertTrue(result is 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()

Notes:

  • 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 assertTrue here to ensure that the temperature value is returned as the same object that was passed and not converted to and from kelvins.

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

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

OK