Unit tests#

The only way to go fast is to go well.
— Robert C. Martin
Slides/PDF#
Functional test#
Unit tests are very important for preventing and diagnosing errors. A unit test tests a single code module, such as a function or a class.
For testing, Python typically uses dedicated packages like unittest
or nosetest
, which are then executed automatically when code changes. Simple tests can be written directly in the code. For this, the assert
statement is used. It checks whether a test condition is met (assert) and raises an AssertionError
exception if this is not the case.
We want, for example, to test the previously defined division function division()
.
def divide(numerator, denominator):
if not isinstance(denominator, (int, float)):
raise ValueError(f"Nenner nicht vom Datentyp `int` oder `float`")
elif not isinstance(numerator, (int, float)):
raise ValueError(f"Zaehler nicht vom Datentyp `int` oder `float`")
elif not denominator:
print("Warnung Division durch 0")
return None
else:
result = numerator / denominator
print(f"Das Ergebnis von {numerator}/{denominator} = {result}")
return result
For this, we first write a functional test. These tests verify the correct functioning of a module with multiple inputs for which we know the correct result. Here you usually perform several tests to ensure that you don’t get the correct result by chance, but rather for different combinations.
def test_function(): # The usual notation for tests in Python is to have function names starting with `test_`
assert division(10, 2) == 5
assert division(10, 5) == 2
assert division(50, 1) == 50
assert division(50, 5) == 10
test_function()
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[3], line 1
----> 1 test_function()
Cell In[2], line 2, in test_function()
1 def test_function(): # The usual notation for tests in Python is to have function names starting with `test_`
----> 2 assert division(10, 2) == 5
3 assert division(10, 5) == 2
4 assert division(50, 1) == 50
NameError: name 'division' is not defined
Limit Test#
Another important test is the boundary value test. Here you check whether inputs in the boundary value range are handled correctly. For example, whether positive or negative infinity is handled, or whether division by zero is handled. We defined above, when creating the function, that in the case of a division by zero the function should output the value None
.
import math # Infinity is defined in the math library
def test_limit():
assert division(math.inf, 1) == math.inf
assert division(math.inf, 100) == math.inf
assert math.isnan(division(math.inf, math.inf)) # Checking for nan (not a number) is only possible through the `isnan` function
assert division(1, math.inf) == 0
assert division(100, 0) is None
test_limit_value()
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[5], line 1
----> 1 test_limit_value()
NameError: name 'test_limit_value' is not defined
Data Type Test#
Since Python supports dynamic typing, type testing is very important. This involves testing whether the function handles inputs with incorrect data types correctly.
We can’t directly use assert
here, because it would crash our code.
assert division("falscher typ", 1) is ValueError
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[6], line 1
----> 1 assert division("falscher typ", 1) is ValueError
NameError: name 'division' is not defined
Instead, in the test we use a try-except
block and the else
clause. It will only be executed if in the try
block no exception occurs, which is undesirable in the test case, so we raise an AssertionError
in this case, since the test case fails. Furthermore, we check whether the exception thrown by the function also has the correct type TypeError
.
def test_datatype():
# we test the numerator
try:
division("falscher typ", 1)
except Exception as e:
assert isinstance(e, ValueError)
else:
raise AssertionError("Datatype")
# the second case of the denominator
try:
division(10, "falscher typ")
except Exception as e:
assert isinstance(e, ValueError)
else:
raise AssertionError("Datatype")
test_data_type()
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[8], line 1
----> 1 test_data_type()
NameError: name 'test_data_type' is not defined
Detecting Errors in Code Changes with Tests#
We deliberately defined these unit tests as functions so that we can run them repeatedly whenever the code changes. This allows us to test whether the implementation actually meets the stated expectations, or still does so.
For example, will the division function be overwritten by a trivial implementation?
def divide(numerator, denominator):
return numerator / denominator
This is still how our functional test works. That’s why it’s usually not enough to implement only this test or to only verify the function during development.
test_function()
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[10], line 1
----> 1 test_function()
Cell In[2], line 2, in test_function()
1 def test_function(): # The usual notation for tests in Python is to have function names starting with `test_`
----> 2 assert division(10, 2) == 5
3 assert division(10, 5) == 2
4 assert division(50, 1) == 50
NameError: name 'division' is not defined
However, the boundary-value test will now throw an exception.
test_grenzwert()
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[11], line 1
----> 1 test_grenzwert()
NameError: name 'test_grenzwert' is not defined
The data type test isn’t successful either, since we’ve defined that a ValueError
should be raised for incorrect input data types, but we’re getting a TypeError
.
test_data_type()
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[12], line 1
----> 1 test_data_type()
NameError: name 'test_data_type' is not defined