Assertions#

The main purpose of assertions is to catch logical errors: we need checks that verify that our code is working correctly.

Assertions can be defined by the keyword assert. They take an expression that evaluates to a boolean and if its value is False, then an AssertionError is raised. We can set a message for the raised exception by providing it in the assertion.

You can think of assertion statements assert <condition>, <message> as equivalent to:

if not condition:
  raise AssertionError(message)

Assertions are very useful for unit testing your code. A unit can be seen as the smallest piece of code that can be individually tested. In our case, this would mean testing a Python function.

It is advisable to make one or multiple test functions that test your code and run those functions every time you make a modification to the codebase (i.e., all the code you develop). This can ensure that any new modifications to the existing code do not damage the previous functionalities.

Below you can find a few examples where we have correct assertions, using the function from above and the calculate_weight function we’ve seen before. Therefore, no exception is occurring. Note that setting a message is not mandatory, but highly recommended for debugging purposes:

def calculate_weight(density, volume):
    """
    Calculates weight of an object, given its density and volume.

    Args:
        density (int): Density of an object.
        volume (int): Volume of an object.

    Returns:
        int: Weight of an object.
    """
    return density * volume
assert calculate_weight(30, 10) == 300
assert calculate_weight(70, 4) == 280, "The weight of an object with density of 70 kg/m^3 and volume of 4 m^3 should be 280 kg."
assert calculate_weight(12, 500) == 6000, "The weight of an object with density of 12 kg/m^3 and volume of 500 m^3 should be 6000 kg."

Below, there is an example of a failing assertion for a function converting Kelvin to Celsius. This assertion should trigger the developer into adjusting the value of the zero_point.

def kelvin_to_celsius(kelvin_temperature):
    """
    Conerts [K] into [°C]

    Args:
        kelvin_temperature (float): Temperature in [K]

    Returns:
        float: Temperature in [°C].
    """
    zero_point = 273
    return kelvin_temperature-zero_point

assert kelvin_to_celsius(0)==-273.15, "0 Kelvin are equal to -273.15 Celsius"
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[3], line 14
     11     zero_point = 273
     12     return kelvin_temperature-zero_point
---> 14 assert kelvin_to_celsius(0)==-273.15, "0 Kelvin are equal to -273.15 Celsius"

AssertionError: 0 Kelvin are equal to -273.15 Celsius

When writing tests we should focus on covering “boundary” values. Those are values that are part of a condition statement. For example, in the statement if weight > 0, the boundary value is 0.

By a rule of thumb, it is customary, to have at least 4 tests for every condition:

  • one that is on the boundary of condition and evaluates it to True (i.e. setting weight to 1)

  • one that is on the boundary and evaluates it to False (i.e. setting weight to 0)

  • one that is not on the boundary and evaluates to True (i.e. setting weight to 100)

  • one that is not on the boundary and evaluates to False (i.e. setting weight to -70)

Moreover, you should try testing with different types as well. For example, floats or strings to observe the behaviour of your code. Finally, you could also consider testing edge cases, such as: infinity, zero, NAN, empty strings, etc.

Below you can find several examples of tests for boundary values:

Exercise#

Try to change any of the values for density and volume to cause an assertion to fail. What do you observe?

def test_calculate_weight(calculate_weight):
    assert calculate_weight(0, 3) == 0, "The weight of an object with density of 0 kg/m^3 and volume of 3 m^3 should be 0 kg."
    assert calculate_weight(50, 0) == 0, "The weight of an object with density of 50 kg/m^3 and volume of 0 m^3 should be 0 kg."
    assert calculate_weight(0, 0) == 0, "The weight of an object with density of 0 kg/m^3 and volume of 0 m^3 should be 0 kg."
    
    assert calculate_weight(100, 1) == 100, "The weight of an object with density of 100 kg/m^3 and volume of 1 m^3 should be 100 kg."
    assert calculate_weight(45, 2) == 90, "The weight of an object with density of 45 kg/m^3 and volume of 2 m^3 should be 90 kg."

test_calculate_weight(calculate_weight)

Exercise#

You are given the method calculate_area again to calculate the shuttering area of a concrete rectangular column. The shuttering is a temporary arrangement done for vertical surfaces to support the wet concrete till it attains the desired strength (source: Civilread.com).

Write assertions in test_calculate_area that can verify that method calculate_area is free of bugs. Make sure you include meaningful messages in case a test fails. Try to write tests that test the boundary values of the if statement. For example, tests that evaluate the condition to True and/or False.

def calculate_area(height, length, breadth):
    """
    Calculates shuttering area of a concrete rectangular column.

    Args:
        height (int): Height of the column.
        length (int): Length of the column.
        breadth (int): Breadth of the column.

    Returns:
        int: The area or -1 if the input is invalid.
    """
    if height <= 0 or length <= 0 or breadth <= 0:
        return -1
    else:
        return (2 * breadth + 2 * length) * height


def test_calculate_area(calculate_area):
    YOUR CODE HERE

test_calculate_area(calculate_area)

Asserts and floating point numbers#

Suppose that we are dealing with floating point numbers in our calculations and we would like to run test on them. Try running the code below and think whether the result makes sense:

assert 0.1 + 0.2 == 0.3
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[24], line 1
----> 1 assert 0.1 + 0.2 == 0.3

AssertionError: 

The assertion above fails due to the fact that we are dealing with floating point numbers. For example, we might be using 0.3 in our calculations, but in practice, 0.30004 could be used instead due to the way floating point numbers are stored in memory. (If interested, have a look at the following link: https://0.30000000000000004.com/).

To mitigate this issue, when making assertions for floating point numbers, you can calculate the absolute value of the difference between the expected and actual values and verify that this difference is smaller than some epsilon value:

epsilon = 1e-6 # exponential notation for 0.000001
assert abs((0.1 + 0.2) - 0.3) < epsilon