1. Unit Testing


Figure 37. Development driven tests

1.1. Glossary


A method stub or simply stub in software development is a piece of code used to stand in for some other programming functionality. A stub may simulate the behavior of existing code (such as a procedure on a remote machine) or be a temporary substitute for yet-to-be-developed code. Stubs are therefore most useful in porting, distributed computing as well as general software development and testing.


In object-oriented programming, mock objects are simulated objects that mimic the behavior of real objects in controlled ways. In a unit test, mock objects can simulate the behavior of complex, real objects and are therefore useful when a real object is impractical or impossible to incorporate into a unit test.


In computer programming, unit testing is a software testing method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use.

1.2. Running tests

1.2.1. Running tests with your IDE

  • View menu -> Run... -> Unittest in my_function

1.2.2. From code

if __name__ == "__main__":
    import unittest

1.2.3. From command line

Listing 286. Display only errors
$ python -m unittest example.py
Listing 287. With -v display progress
python -m unittest -v example.py

1.3. Example

1.3.1. Example 1

Listing 288. Example unittest code coverage

from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Optional
from unittest import TestCase

class User:
    first_name: str
    last_name: str
    date_of_birth: Optional[datetime] = None
    permission: list = ()

    def __post_init__(self):
        self.permission = list(self.permission)

        if self.date_of_birth and self.date_of_birth.tzinfo != timezone.utc:
            raise ValueError

    def add_permission(self, permission):

    def remove_permission(self, permission):

    def __str__(self):
        return f'User(first_name="{self.first_name}", last_name="{self.last_name}")'

class UserTest(TestCase):

    def setUpClass(cls) -> None:

    def tearDownClass(cls) -> None:

    def setUp(self) -> None:
        now = datetime.now(tz=timezone.utc)
        self.user = User(first_name='Jan', last_name='Twardowski', date_of_birth=now)

    def tearDown(self) -> None:

    def test_create_user(self):
        user = User(first_name='Jan', last_name='Twardowski')
        self.assertEqual(user.first_name, 'Jan')
        self.assertEqual(user.last_name, 'Twardowski')

    def test_permission_add(self):
        self.assertIn('read', self.user.permission)

    def test_permission_remove(self):
        self.assertNotIn('read', self.user.permission)

    def test_date_of_birth_in_utc(self):
        self.assertEqual(self.user.date_of_birth.tzinfo, timezone.utc)

    def test_date_of_birth_not_in_utc(self):
        with self.assertRaises(ValueError):
            now = datetime.now()
            user = User(first_name='Jan', last_name='Twardowski', date_of_birth=now)
            self.assertEqual(user.date_of_birth.tzinfo, timezone.utc)

    def test_str(self):
        self.assertEqual(str(self.user), 'User(first_name="Jan", last_name="Twardowski")')

1.3.2. Example 2

Listing 289. Example unittest code coverage
from unittest import TestCase

class Temperature:
    def __init__(self, kelvin=None, celsius=None, fahrenheit=None):
        values = [x for x in [kelvin, celsius, fahrenheit] if x]

        if len(values) < 1:
            raise ValueError('Need argument')

        if len(values) > 1:
            raise ValueError('Only one argument')

        if celsius is not None:
            self.kelvin = celsius + 273.15
        elif fahrenheit is not None:
            self.kelvin = (fahrenheit - 32) * 5 / 9 + 273.15
            self.kelvin = kelvin

        if self.kelvin < 0:
            raise ValueError('Temperature in Kelvin cannot be negative')

    def __str__(self):
        return f'Temperature = {self.kelvin} Kelvins'

class TemperatureTest(TestCase):
    def test_creating_temperature(self):
        with self.assertRaises(ValueError):

    def test_setting_temperature(self):
        temp = Temperature(10)
        self.assertEqual(temp.kelvin, 10)

    def test_temp_from_celsius(self):
        temp = Temperature(celsius=1)
        self.assertEqual(temp.kelvin, 274.15)

    def test_temp_from_fahrenheit(self):
        temp = Temperature(fahrenheit=1)
        self.assertAlmostEqual(temp.kelvin, 255.928, places=3)

    def test_invalid_initialization(self):
        with self.assertRaises(ValueError):
            Temperature(celsius=1, kelvin=1)

    def test_negative_kelvins(self):
        with self.assertRaises(ValueError):

        with self.assertRaises(ValueError):

        with self.assertRaises(ValueError):

    def test_to_string(self):
        temp = Temperature(kelvin=10)
        self.assertEqual(str(temp), 'Temperature = 10 Kelvins')

        temp = Temperature(celsius=10)
        self.assertEqual(str(temp), 'Temperature = 283.15 Kelvins')

1.3.3. Example 3

Listing 290. Example unittest code coverage
from dataclasses import dataclass
from unittest import TestCase

class Longitude:
    value: float

    def __post_init__(self):
        if self.value > 180:
            raise ValueError
        if self.value < -180:
            raise ValueError

class Latitude:
    value: float

    def __post_init__(self):
        if self.value > 90:
            raise ValueError
        if self.value < -90:
            raise ValueError

class GEOCoordinates:
    lat: Latitude
    lon: Longitude

class LongitudeTest(TestCase):
    def test_init_latitude(self):
        l = Latitude(0)
        self.assertEqual(l.value, 0)

    def test_invalid(self):
        with self.assertRaises(ValueError):

        with self.assertRaises(ValueError):

class LatitudeTest(TestCase):
    def test_init_latitude(self):
        l = Latitude(0)
        self.assertEqual(l.value, 0)

    def test_invalid(self):
        with self.assertRaises(ValueError):

        with self.assertRaises(ValueError):

class GEOCoordinatesTest(TestCase):
    def test_set_longitude(self):
        lat = Latitude(-90)
        lon = Longitude(20)
        geo = GEOCoordinates(lat, lon)

1.3.4. Example 4

Listing 291. Example unittest code coverage
from dataclasses import dataclass
from datetime import date, datetime, timezone
from typing import Optional
from unittest import TestCase

class Astronaut:
    name: str
    agency: str = 'NASA'
    date_of_birth: Optional[date] = None
    first_step: Optional[datetime] = None

    def __str__(self):
        return f'My name... {self.name}'

    def __post_init__(self):
        if self.first_step and self.first_step.tzinfo != timezone.utc:
            raise ValueError('Timezone must by UTC')

class AstronautTest(TestCase):
    def setUp(self):
        self.astro = Astronaut(name='Jose Jimenez', agency='NASA')

    def test_recruiting_new_astronaut(self):
        jose = Astronaut(name='Jose Jimenez')
        self.assertEqual(jose.name, 'Jose Jimenez')

    def test_default_agency(self):
        jose = Astronaut(name='Jose Jimenez')
        self.assertEqual(jose.agency, 'NASA')

    def test_date_of_birth(self):
        jose = Astronaut(name='Jose Jimenez', date_of_birth=date(1961, 4, 12))
        self.assertEqual(jose.date_of_birth, date(1961, 4, 12))

    def test_first_step_in_utc(self):
        step = datetime(1969, 7, 21, 14, tzinfo=timezone.utc)
        jose = Astronaut(name='Jose Jimenez', first_step=step)
        self.assertEqual(jose.first_step.tzinfo, timezone.utc)

    def test_first_step_not_in_utc(self):
        step = datetime(1969, 7, 21, 14)

        with self.assertRaises(ValueError):
            Astronaut(name='Jose Jimenez', first_step=step)

    def test_hello(self):
        self.assertEqual(str(self.astro), 'My name... Jose Jimenez')

1.3.5. Example 5

Listing 292. Example unittest code coverage
import unittest
from typing import Union

class Rectangle:
    def __init__(self, bok_a: Union[float, int], bok_b: [float, int]) -> None:
        if not isinstance(bok_a, (float, int)) or bok_a <= 0:
            raise ValueError('Side A cannot be negative')

        if not isinstance(bok_b, (float, int)) or bok_b <= 0:
            raise ValueError('Side B cannot be negative')

        self.side_a = float(bok_a)
        self.side_b = float(bok_b)

    def area(self) -> float:
        return self.side_a * self.side_b

    def circumference(self) -> float:
        return 2 * (self.side_a + self.side_b)

    def __str__(self) -> str:
        return f'Rectangle(a={self.side_a}, b={self.side_b})'

class RectangleTest(unittest.TestCase):

    def setUp(self):
        self.rectangle = Rectangle(bok_a=10, bok_b=20)

    def test_create_rectangle(self):
        Rectangle(bok_a=5, bok_b=10)

    def test_create_rectangle_with_invalid_side(self):
        with self.assertRaises(ValueError):
            Rectangle(bok_a='a', bok_b=20)

        with self.assertRaises(ValueError):
            Rectangle(bok_a=20, bok_b='b')

        with self.assertRaises(ValueError):
            Rectangle(bok_a='b', bok_b='b')

    def test_create_rectangle_side_zero(self):
        with self.assertRaises(ValueError):
            Rectangle(bok_a=0, bok_b=20)

        with self.assertRaises(ValueError):
            Rectangle(bok_a=20, bok_b=0)

        with self.assertRaises(ValueError):
            Rectangle(bok_a=0, bok_b=0)

    def test_create_rectangle_side_negative(self):
        with self.assertRaises(ValueError):
            Rectangle(bok_a=-3, bok_b=20)

        with self.assertRaises(ValueError):
            Rectangle(bok_a=20, bok_b=-3)

        with self.assertRaises(ValueError):
            Rectangle(bok_a=-1, bok_b=-3)

    def test_create_rectangle_with_one_side(self):
        with self.assertRaises(TypeError):

        with self.assertRaises(TypeError):

    def test_create_rectangle_and_store_values(self):
        p = Rectangle(bok_a=5, bok_b=10)
        self.assertEqual(p.side_a, 5)
        self.assertEqual(p.side_b, 10)

    def test_create_rectangle_valid(self):
        self.assertEqual(self.rectangle.side_a, 10)
        self.assertEqual(self.rectangle.side_b, 20)

    def test_area(self):
        self.assertEqual(self.rectangle.area(), 200.0)

    def test_circumference(self):
        self.assertEqual(self.rectangle.circumference(), 60)

    def test_stringify_rectangle(self):
        self.assertEqual(str(self.rectangle), 'Rectangle(a=10.0, b=20.0)')

if __name__ == '__main__':

1.4. Assignments

1.4.1. Dragon

  • Complexity level: medium

  • Lines of code to write: 100 lines

  • Estimated time of completion: 25 min

  1. Write unittest for the dragon created in exam at the end of OOP Syntax chapter

  1. Napisz testy jednostkowe dla Smoka z egzaminu na końcu rozdziału OOP Syntax