Unit tests#

Midjourney: Waves testing a boat, ref. Hokusai

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

Quiz#

shuffleQuestions: true shuffleAnswers: true ### What is a bug? - [x] An unintended error in the program. - [ ] A special feature of a debugger. - [ ] A program that tests other programs. - [ ] A function that is particularly hard to understand. ### What is a unit test in Python? - [x] A test for a single function or class - [ ] A test for an entire program - [ ] A visual testing tool - [ ] A form of documentation ### What happens if an `assert` fails? - [ ] The test is skipped - [x] An `AssertionError` is raised - [ ] The code continues to run - [ ] The program ends without an error message ### Why are unit tests used? - [x] To detect errors early - [ ] To automatically format code - [ ] To reduce memory usage - [ ] To improve the user interface ### Which packages are commonly used in Python for unit testing? - [x] `unittest` - [x] `nosetest` - [ ] `math` - [ ] `pytest-ui` ### What types of testing strategies are there? - [x] Boundary value test - [x] Functional test - [x] Data type test - [ ] Syntax test