Testing

When we write code, we want to be sure that it is doing exactly what we thinking it is doing. Unfortunately, no one is perfect and this property is transferred to the code that we write. So we, as conscious, programmers, should write tests for all of your code ensuring that is it doing the right thing in all circumstances.

Typically we apply tests for our code that the function level, where there is, at least a test for each of the functions in our program. However, in order to achieve full test coverage, it may be necessary to have more than one test for a given function. Consider the code below, which performs simple temperature conversions, using logic to change the arithmetic as appropriate.

def temperature_conversion(value, unit):
    """
    Temperature conversion that is dependent on 
    the input unit. 
    
    Args:
        value (float): Temperature to be converted. 
        unit (str): Unit of the original temperature.
    
    Returns:
        (float): Temperature converted to Kelvin.
    """
    if unit == 'C':
        return value + 273.15
    elif unit == 'F':
        return (value + 459.67) * 5 / 9
    else:
        raise ValueError("The supported units are C or F.")

In this example, our code will raise an error if the unit argument is neither 'C' or 'F'.

We can now write some tests for this function. We know that the melting point is water is 0 oC, 273.15 K and 32 oF. Therefore our tests should reflect this knowledge.

import numpy as np
c = temperature_conversion(0, 'C')
np.testing.assert_almost_equal(c, 273.15)
print('Test 1 Passed!')
Test 1 Passed!
f = temperature_conversion(32, 'F')
np.testing.assert_almost_equal(f, 273.15)
print('Test 2 Passed!')
Test 2 Passed!

These two tests will cover two of the branches of our function. The np.testing.assert_almost_equal function is an element of the NumPy testing library. This test function checks if the two arguments are approximately equal. This must be approximate due to the floating point arithmetic problems. For an integer, string, or Boolean where the value does not need to be approximate, the np.testing.assert_equal function may be used. If the values are not almost equal, and error will be thrown.

np.testing.assert_almost_equal(c, 270.15)
print('Test 3 Passed!')
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
/tmp/ipykernel_1967/3447443753.py in <module>
----> 1 np.testing.assert_almost_equal(c, 270.15)
      2 print('Test 3 Passed!')

/opt/hostedtoolcache/Python/3.7.12/x64/lib/python3.7/site-packages/numpy/testing/_private/utils.py in assert_almost_equal(actual, desired, decimal, err_msg, verbose)
    597         pass
    598     if abs(desired - actual) >= 1.5 * 10.0**(-decimal):
--> 599         raise AssertionError(_build_err_msg())
    600 
    601 

AssertionError: 
Arrays are not almost equal to 7 decimals
 ACTUAL: 273.15
 DESIRED: 270.15

Notice that the Python kernel never reaches the print statement.

However, so far our tests do not cover every possible operation of the function. We have a test for the if and elif branches, but not the else branch. Therefore, we must introduce another type of test, that checks if an error is raised.

np.testing.assert_raises(ValueError, temperature_conversion, 0, 'c')
print('Test 4 Passed!')
Test 4 Passed!

This function takes the expected error type, the function and the input arguments and will pass if the error is thrown under these circumstances.

Test-driven development

Test-driven development (TDD) is a methodology for computer programming, where the tests are written first for a particular use case and then the operational code is written such that it passes these tests. This methodology is different from that applied to this point in the course, however, it is very popular and powerful when applied properly. In the exercise below, you are asked to try your hand at some test-driven development.