16.2. Test Unittest

../../_images/fun-geekandpoke-development-driven-tests.jpg

Figure 16.1. Development driven tests

>>> def is_adult(age: int|float) -> bool:
...     if age >= 18:
...         return True
...     else:
...         return False
>>>
>>>
>>> def test_isadult_minor():
...     assert is_adult(10) == False
>>>
>>> def test_isadult_adult():
...     assert is_adult(30) == True
>>>
>>> def test_isadult_almost():
...     assert is_adult(17.99) == False
>>>
>>> def test_isadult_barely():
...     assert is_adult(18.00) == True

16.2.1. Glossary

Stub

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.

Mock

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.

Unittest

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.

16.2.2. Running tests with your IDE

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

16.2.3. From code

if __name__ == "__main__":
    import unittest
    unittest.main()

16.2.4. From command line

Display only errors. With -v display progress:

$ python -m unittest myfile.py
$ python -m unittest -v myfile.py

16.2.5. Methods

  • self.assertEqual()

  • self.assertAlmostEqual(0.1+0.2, 0.3, places=16)

  • self.assertTrue()

  • self.assertFalse()

  • self.assertDictEqual()

  • self.assertIn()

  • self.assertIs()

  • self.assertIsInstance()

  • self.assertIsNotNone()

  • self.assertRaises()

16.2.6. Example 1

Code 16.3. Example unittest

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


@dataclass
class User:
    firstname: str
    lastname: str
    date_of_birth: datetime | None = 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):
        self.permission.append(permission)

    def remove_permission(self, permission):
        self.permission.remove(permission)

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


class UserTest(TestCase):

    @classmethod
    def setUpClass(cls) -> None:
        pass

    @classmethod
    def tearDownClass(cls) -> None:
        pass

    def setUp(self) -> None:
        now = datetime.now(tz=timezone.utc)
        self.user = User(firstname='Mark', lastname='Watney', date_of_birth=now)

    def tearDown(self) -> None:
        pass

    def test_create_user(self):
        user = User(firstname='Mark', lastname='Watney')
        self.assertEqual(user.firstname, 'Mark')
        self.assertEqual(user.lastname, 'Watney')

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

    def test_permission_remove(self):
        self.user.add_permission('read')
        self.user.remove_permission('read')
        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(firstname='Mark', lastname='Watney', date_of_birth=now)
            self.assertEqual(user.date_of_birth.tzinfo, timezone.utc)

    def test_str(self):
        self.assertEqual(str(self.user), 'User(firstname="Mark", lastname="Watney")')

16.2.7. Example 2

Code 16.4. Example unittest
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
        else:
            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):
            Temperature()

    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):
            Temperature(kelvin=-1)

        with self.assertRaises(ValueError):
            Temperature(celsius=-274)

        with self.assertRaises(ValueError):
            Temperature(fahrenheit=-1000)

    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')

16.2.8. Example 3

Code 16.5. Example unittest
from dataclasses import dataclass
from unittest import TestCase


@dataclass
class Longitude:
    value: float

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


@dataclass
class Latitude:
    value: float

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


@dataclass
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):
            Latitude(-180.1)

        with self.assertRaises(ValueError):
            Latitude(180.1)


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

    def test_invalid(self):
        with self.assertRaises(ValueError):
            Latitude(-90.1)

        with self.assertRaises(ValueError):
            Latitude(90.1)


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

16.2.9. Example 4

Code 16.6. Example unittest
from dataclasses import dataclass
from datetime import date, datetime, timezone
from unittest import TestCase


@dataclass
class Astronaut:
    name: str
    agency: str = 'NASA'
    date_of_birth: date | None = None
    first_step: datetime | None = 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.mark = Astronaut(name='José Jiménez', agency='NASA')

    def test_recruiting_new_astronaut(self):
        jose = Astronaut(name='José Jiménez')
        self.assertEqual(jose.name, 'José Jiménez')

    def test_default_agency(self):
        jose = Astronaut(name='José Jiménez')
        self.assertEqual(jose.agency, 'NASA')

    def test_date_of_birth(self):
        jose = Astronaut(name='José Jiménez', 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='José Jiménez', 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='José Jiménez', first_step=step)

    def test_hello(self):
        self.assertEqual(str(self.astro), 'My name... José Jiménez')

16.2.10. Example 5

Code 16.7. Example unittest
import unittest


class Rectangle:
    def __init__(self, bok_a: 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):
            Rectangle(bok_a=0)

        with self.assertRaises(TypeError):
            Rectangle(bok_b=0)

    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__':
    unittest.main()

16.2.11. Example 6

Code 16.8. Example unittest
class Point:
    x: int
    y: int

    def __init__(self, x: int = 0, y: int = 0) -> None:
        self.x = x
        self.y = y

    def __str__(self) -> str:
        return f'({self.x}, {self.y})'

    def __repr__(self) -> str:
        return f'Point(x={self.x}, y={self.y})'

    def get_coordinates(self) -> tuple[int,int]:
        return self.x, self.y

    def set_coordinates(self, x: int = None, y: int = None) -> None:
        if x is None or y is None:
            raise TypeError
        self.x = x
        self.y = y


## Tests
from unittest import TestCase


class PointInitTest(TestCase):

    def test_init_noargs(self):
        pt = Point()
        self.assertIsInstance(pt, Point)

    def tet_init_default(self):
        pt = Point()
        self.assertEqual(pt.x, 0)
        self.assertEqual(pt.y, 0)

    def test_init_args(self):
        pt = Point(1, 2)
        self.assertEqual(pt.x, 1)
        self.assertEqual(pt.y, 2)

    def test_init_kwargs(self):
        pt = Point(x=1, y=2)
        self.assertEqual(pt.x, 1)
        self.assertEqual(pt.y, 2)


class PointOtherTest(TestCase):

    def setUp(self) -> None:
        self.pt = Point(x=1, y=2)

    def test_str(self):
        self.assertEqual(str(self.pt), '(1, 2)')

    def test_repr(self):
        self.assertEqual(repr(self.pt), 'Point(x=1, y=2)')

    def test_get_coordinates(self):
        self.assertEqual(self.pt.get_coordinates(), (1, 2))

    def test_set_coordinates_args(self):
        with self.assertRaises(TypeError):
            self.pt.set_coordinates()

    def test_set_coordinates_kwargs(self):
        self.pt.set_coordinates(x=10, y=20)
        self.assertEqual(self.pt.x, 10)
        self.assertEqual(self.pt.y, 20)

16.2.12. Example 7

Code 16.9. Example unittest
from unittest import TestCase


class User:
    firstname: str
    lastname: str
    age: int | None

    def __init__(self, firstname: str, lastname: str,
                 *, age: int | None = None) -> None:
        self.firstname = firstname
        self.lastname = lastname
        self.age = age
        self.salary = None
        self.job = None

    def say_hello(self) -> str:
        return f'Hello {self.firstname} {self.lastname}'

    def __str__(self) -> str:
        return f'{self.firstname} {self.lastname}'

    def __repr__(self) -> str:
        clsname = self.__class__.__name__
        firstname = self.firstname
        lastname = self.lastname
        return f'{clsname}({firstname=}, {lastname=})'

    def make_older(self, years: int) -> None:
        self.age += years

    def is_astronaut(self):
        if self.job == 'astronaut':
            return True
        else:
            return False

    def is_rich(self):
        if self.salary <= 5000:
            return False
        return True


class UserInitTest(TestCase):

    def test_init_name(self) -> None:
        user = User('Mark', 'Watney')
        self.assertEqual(user.firstname, 'Mark')
        self.assertEqual(user.lastname, 'Watney')

    def test_init_age_positional(self) -> None:
        with self.assertRaises(TypeError):
            User('Mark', 'Watney', 40)  # noqa

    def test_init_age_keyword(self) -> None:
        user = User('Mark', 'Watney', age=40)
        self.assertEqual(user.age, 40)


class UserFeaturesTest(TestCase):

    def setUp(self) -> None:
        self.user = User('Mark', 'Watney', age=40)

    def test_make_older(self):
        self.user.make_older(years=1)
        self.assertEqual(self.user.age, 41)

    def test_hello(self) -> None:
        self.assertEqual(self.user.say_hello(), 'Hello Mark Watney')

    def test_str(self) -> None:
        self.assertEqual(str(self.user), 'Mark Watney')

    def test_repr(self) -> None:
        self.assertEqual(repr(self.user), "User(firstname='Mark', lastname='Watney')")


class UserJobTest(TestCase):

    def setUp(self) -> None:
        self.user = User('Mark', 'Watney', age=40)

    def test_job_default(self) -> None:
        self.assertEqual(self.user.job, None)

    def test_job_isastronaut_when_job_default(self) -> None:
        self.assertFalse(self.user.is_astronaut())

    def test_job_isastronaut_when_job_other(self) -> None:
        self.user.job = 'other'
        self.assertFalse(self.user.is_astronaut())

    def test_job_isastronaut_when_job_astronaut(self) -> None:
        self.user.job = 'astronaut'
        self.assertTrue(self.user.is_astronaut())


class UserSalaryTest(TestCase):

    def setUp(self) -> None:
        self.user = User('Mark', 'Watney', age=40)

    def test_salary_default(self):
        self.assertIsNone(self.user.salary)
        self.assertEqual(self.user.salary, None)

    def test_salary_field(self):
        self.user.salary = 10_000.00
        self.assertAlmostEqual(self.user.salary, 10_000, places=2)

    def test_salary_isrich_negative(self):
        self.user.salary = 1000.00
        self.assertFalse(self.user.is_rich())

    def test_salary_isrich_positive(self):
        self.user.salary = 10_000.00
        self.assertTrue(self.user.is_rich())

16.2.13. Example 8

Code 16.10. Example unittest
from datetime import date
from unittest import TestCase


class User:
    def __init__(self, name, /, *, birthday=None):
        self.name = name
        self.birthday = date.fromisoformat(birthday) if birthday else None

    def age(self):
        YEAR = 365.25
        today = date.today()
        age = (today - self.birthday).days / YEAR
        return round(age)


class UserTest(TestCase):

    # Initialization tests

    def test_init_noname(self):
        with self.assertRaises(TypeError):
            User()  # noqa

    def test_init_name_positional(self):
        u = User('Mark')
        self.assertEqual(u.name, 'Mark')

    def test_init_name_keyword(self):
        with self.assertRaises(TypeError):
            User(name='Mark')  # noqa

    def test_init_birthday_positional(self):
        with self.assertRaises(TypeError):
            User('Mark', '1969-07-21')  # noqa

    def test_init_birthday_keyword(self):
        u = User('Mark', birthday='1969-07-21')
        self.assertEqual(u.birthday, date(1969, 7, 21))

    # Other tests

    def setUp(self) -> None:
        self.user = User('Mark', birthday='1969-07-21')

    def test_age(self):
        today = date(2000, 1, 1)
        self.assertEqual(self.user.age(), 53)

16.2.14. Example 9

Code 16.11. Example unittest
import os
from datetime import date
from unittest import TestCase


# Operating system will control this env variable
os.environ['STAGE'] = 'test'


STAGE = os.getenv('STAGE', default='production')

SECOND = 1
MINUTE = 60 * SECOND
HOUR = 60 * MINUTE
DAY = 24 * HOUR
WEEK = 7 * DAY
YEAR = 365.25 * DAY
MONTH = YEAR / 12


def today():
    if STAGE == 'test':
        return date(2000, 1, 1)
    else:
        return date.today()


class User:
    username: str
    password: str
    email: str | None
    birthday: date | None
    authenticated: bool

    def __init__(self, *,
                 username: str,
                 password: str,
                 email: str | None = None,
                 birthday: str | None = None,
                 ) -> None:
        self.username = username
        self.password = password
        self.email = email
        self.birthday = date.fromisoformat(birthday) if birthday else None
        self.authenticated = False

    @property
    def age(self) -> int:
        years = (today() - self.birthday).total_seconds() / YEAR
        return int(years)

    def __str__(self) -> str:
        return f'{self.username}'

    def __repr__(self) -> str:
        clsname = self.__class__.__name__
        username = self.username
        return f'{clsname}({username=})'

    def login(self) -> None:
        self.authenticated = True


class UserInitTest(TestCase):

    def test_init_user_keyword(self):
        user = User(username='mwatney', password='Ares3')
        self.assertEqual(user.username, 'mwatney')
        self.assertEqual(user.password, 'Ares3')

    def test_init_user_positional(self):
        with self.assertRaises(TypeError):
            User('mwatney', 'Ares3')  # noqa

    def test_init_email(self):
        user = User(username='mwatney', password='Ares3', email='mwatney@nasa.gov')
        self.assertEqual(user.email, 'mwatney@nasa.gov')

    def test_init_birthday(self):
        user = User(username='mwatney', password='Ares3', birthday='1969-07-21')
        self.assertEqual(user.birthday, date(1969, 7, 21))

    def test_init_annotations(self):
        user = User(username='mwatney', password='Ares3')
        attributes = sorted(vars(user).keys())
        annotations = sorted(User.__annotations__.keys())
        self.assertListEqual(annotations, attributes)


class UserFeatureTest(TestCase):
    def setUp(self) -> None:
        self.user = User(
            username='mwatney',
            password='Ares3',
            email='mwatney@nasa.gov',
            birthday='1969-07-21',
        )

    def test_age(self):
        self.assertEqual(self.user.age, 30)

    def test_str(self):
        self.assertEqual(str(self.user), 'mwatney')

    def test_repr(self):
        self.assertEqual(repr(self.user), "User(username='mwatney')")


class UserAuthTest(TestCase):
    def setUp(self) -> None:
        self.user = User(
            username='mwatney',
            password='Ares3',
            email='mwatney@nasa.gov',
            birthday='1969-07-21',
        )

    def test_password_hidden(self):
        self.assertNotIn('password', str(self.user))
        self.assertNotIn('password', repr(self.user))
        self.assertNotIn(self.user.password, str(self.user))
        self.assertNotIn(self.user.password, repr(self.user))

    def test_authenticated_default(self):
        self.assertFalse(self.user.authenticated)

    def test_login(self):
        self.user.authenticated = False
        self.user.login()
        self.assertTrue(self.user.authenticated)

16.2.15. Example 10

Code 16.12. Example unittest
from datetime import date
from unittest import TestCase
from os import getenv


SECOND = 1
MINUTE = 60 * SECOND
HOUR = 60 * MINUTE
DAY = 24 * HOUR
WEEK = 7 * DAY
YEAR = 365.25 * DAY
MONTH = YEAR / 12


def today():
    env = getenv('ENVIRONMENT', default='prod')
    if env == 'test':
        return date(2000, 1, 1)
    else:
        return date.today()


class User:
    firstname: str
    lastname: str
    birthday: date | None

    def __init__(self, firstname: str, lastname: str, /,
                 *, birthday: str | None = None,
                 ) -> None:
        self.firstname = firstname
        self.lastname = lastname
        self.birthday = date.fromisoformat(birthday) if birthday else None

    @property
    def age(self) -> int:
        days = (today() - self.birthday).total_seconds()
        return int(days/YEAR)

    def say_hello(self) -> str:
        return 'hello'

    def say_goodbye(self) -> str:
        return 'goodbye'

    def __str__(self) -> str:
        return f'{self.firstname} {self.lastname}'

    def __repr__(self) -> str:
        clsname = self.__class__.__name__
        firstname = f"'{self.firstname}'"
        lastname = f"'{self.lastname}'"
        birthday = self.birthday.strftime('%Y-%m-%d')
        return f'{clsname}({firstname}, {lastname}, {birthday=})'


class UserInitTest(TestCase):

    def test_init_noname(self):
        with self.assertRaises(TypeError):
            User()  # noqa
        with self.assertRaises(TypeError):
            User('Mark')  # noqa
        with self.assertRaises(TypeError):
            User('Watney')  # noqa
        with self.assertRaises(TypeError):
            User(lastname='Watney')  # noqa
        with self.assertRaises(TypeError):
            User(lastname='Watney')  # noqa

    def test_init_firstname_and_lastname_positional(self):
        user = User('Mark', 'Watney')
        self.assertIsInstance(user, User)
        self.assertEqual(user.firstname, 'Mark')
        self.assertEqual(user.lastname, 'Watney')

    def test_init_firstname_and_lastname_keyword(self):
        with self.assertRaises(TypeError):
            User(firstname='Mark', lastname='Watney')  # noqa

    def test_init_birthday_positional(self):
        with self.assertRaises(TypeError):
            User('Mark', 'Watney', '1969-07-21')  # noqa

    def test_init_birthday_keyword(self):
        user = User('Mark', 'Watney', birthday='1969-07-21')
        self.assertIsInstance(user.birthday, date)
        self.assertEqual(user.birthday, date(1969, 7, 21))


class UserFeatureTest(TestCase):

    def setUp(self) -> None:
        self.user = User('Mark', 'Watney', birthday='1969-07-21')

    def test_age(self):
        self.assertEqual(self.user.age, 30)

    def test_say_hello(self):
        self.assertEqual(self.user.say_hello(), 'hello')

    def test_say_goodbye(self):
        self.assertEqual(self.user.say_goodbye(), 'goodbye')

    def test_str(self):
        self.assertEqual(str(self.user), 'Mark Watney')

    def test_repr(self):
        self.assertEqual(repr(self.user), "User('Mark', 'Watney', birthday='1969-07-21')")

16.2.16. Example 11

Code 16.13. Example unittest
"""
>>> mark = User('mwatney', password='Ares3', birthday='1969-07-21')

>>> mark.say_hello()
'hello'

>>> mark.get_age()
54

>>> mark.say_secret()
Traceback (most recent call last):
PermissionError: Not authenticated

>>> mark.login(username='mwatney', password='Ares3')

>>> mark.say_secret()
'myVoiceIsMyPassword'
"""

import datetime
from datetime import date
from hashlib import sha256
from typing import NoReturn
from unittest import TestCase, mock

YEAR = 365.25


def encrypt(string) -> str:
    in_bytes = string.encode()
    return sha256(in_bytes).hexdigest()


class User:
    username: str
    password: str | None
    birthday: date | None
    authenticated: bool

    def __init__(self, username: str, /,
                 *, password: str | None = None,
                 birthday: str | None = None,
                 ) -> None:
        self.username = username
        self.password = encrypt(password) if password else None
        self.birthday = date.fromisoformat(birthday) if birthday else None
        self.authenticated = False

    def say_hello(self) -> str:
        return 'hello'

    def get_age(self) -> int:
        today = datetime.date.today()
        days = (today - self.birthday).days
        return int(days / YEAR)

    def __str__(self) -> str:
        return f'{self.username}'

    def __repr__(self) -> str:
        clsname = self.__class__.__name__
        username = self.username
        return f"{clsname}('{username}')"

    def login(self, username: str, password: str) -> None | NoReturn:
        valid_username = self.username == username
        valid_password = self.password == encrypt(password)
        if valid_username and valid_password:
            self.authenticated = True
        else:
            raise PermissionError('Incorrect username or/and password')

    def say_secret(self) -> str | NoReturn:
        if self.authenticated:
            return 'myVoiceIsMyPassword'
        else:
            raise PermissionError('Not authenticated')


class UserInitTest(TestCase):
    def test_init_username_default(self):
        with self.assertRaises(TypeError):
            User()  # noqa

    def test_init_username_positional(self):
        user = User('myusername')
        self.assertEqual(user.username, 'myusername')

    def test_init_username_keyword(self):
        with self.assertRaises(TypeError):
            User(username='myusername')  # noqa

    def test_init_password_default(self):
        user = User('myusername')
        self.assertIsNone(user.password)

    def test_init_password_positional(self):
        with self.assertRaises(TypeError):
            User('myusername', 'valid')  # noqa

    def test_init_password_keyword(self):
        user = User('myusername', password='valid')
        sha256_hash = 'ec654fac9599f62e79e2706abef23dfb7c07c08185aa86db4d8695f0b718d1b3'
        self.assertEqual(user.password, sha256_hash)

    def test_init_birthday_default(self):
        user = User('myusername')
        self.assertIsNone(user.birthday)

    def test_init_birthday_positional(self):
        with self.assertRaises(TypeError):
            User('myusername', '2000-01-01')  # noqa

    def test_init_birthday_keyword(self):
        user = User('myusername', birthday='2000-01-01')
        self.assertIsInstance(user.birthday, date)
        self.assertEqual(user.birthday, date(2000, 1, 1))


class UserFeatureTest(TestCase):
    def setUp(self) -> None:
        self.user = User('myusername', birthday='2000-01-01')

    def test_sayhello(self):
        text = self.user.say_hello()
        self.assertEqual(text, 'hello')

    def test_age(self):
        today = date(2010,1,1)
        with mock.patch('datetime.date') as d:
            d.today.return_value = today
            age = self.user.get_age()
        self.assertIsInstance(age, int)
        self.assertIn(age, range(0, 130))
        self.assertEqual(age, 10)

    def test_str(self):
        text = str(self.user)
        self.assertEqual(text, 'myusername')

    def test_repr(self):
        text = repr(self.user)
        self.assertEqual(text, "User('myusername')")

    def test_saysecret_not_authenticated(self):
        self.user.authenticated = False
        with self.assertRaises(PermissionError):
            self.user.say_secret()

    def test_saysecret_authenticated(self):
        self.user.authenticated = True
        text = self.user.say_secret()
        self.assertEqual(text, 'myVoiceIsMyPassword')


class UserAuthenticationTest(TestCase):
    def setUp(self) -> None:
        self.user = User('myusername', password='valid')

    def test_authentication_default(self):
        self.assertFalse(self.user.authenticated)

    def test_authentication_login_valid(self):
        self.user.login(username='myusername', password='valid')
        self.assertTrue(self.user.authenticated)

    def test_authentication_login_invalid(self):
        with self.assertRaises(PermissionError):
            self.user.login(username='myusername', password='invalid')
        self.assertFalse(self.user.authenticated)


class PasswordTest(TestCase):
    def test_password_encrypt_sha256(self):
        encrypted = encrypt('valid')
        self.assertEqual(encrypted, 'ec654fac9599f62e79e2706abef23dfb7c07c08185aa86db4d8695f0b718d1b3')

16.2.17. Example 12

Code 16.14. Example unittest
import hashlib
from datetime import date
from unittest import TestCase

def password_encode(username: str | None, password: str | None) -> str:
    if username is None and password is None:
        raise ValueError('Username and password cannot be none at the same time')
    string = f'{username}:{password}'
    return hashlib.sha256(string.encode()).hexdigest()


class User:
    username: str
    password: str | None
    birthday: date | None

    def __init__(self, username: str, /, *, password: str | None = None, birthday: str | date | None = None) -> None:
        self.username = username
        self.password = password_encode(username, password)
        self.birthday = date.fromisoformat(birthday) if birthday else None
        self._authenticated = False

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

    def __repr__(self):
        clsname = self.__class__.__name__
        username = self.username
        birthday = self.birthday.isoformat()
        return f"{clsname}('{username}', {birthday=})"

    def say_hello(self) -> str:
        return 'hello'

    def is_authenticated(self) -> bool:
        return self._authenticated

    def logout(self) -> None:
        self._authenticated = False

    def login(self, password: str) -> None:
        encrypted = password_encode(self.username, password)
        if self.password == encrypted:
            self._authenticated = True


class PasswordEncoderTest(TestCase):
    def test_encode_username_and_password(self):
        result = password_encode('myusername', 'validpassword')
        self.assertEqual(result, '37ac6a95b12ae0fb66676e1b7b47e7cffca2b957d2057c14ad8b02e7def2cea3')

    def test_encode_empty_password(self):
        result = password_encode('myusername', None)
        self.assertEqual(result, '10c8b03c7b173081a489538b2cbcf16776ef547377dd99d4737d065b1ab4c17e')

    def test_encode_empty_username(self):
        result = password_encode(None, 'validpassword')
        self.assertEqual(result, '80881add7148cb7f7835994f984f816c2fb936989bee075e8d9c5de613857030')

    def test_encode_empty_username_and_password(self):
        with self.assertRaises(ValueError):
            password_encode(None, None)


class UserInitTest(TestCase):
    def test_init_username_default(self):
        with self.assertRaises(TypeError):
            User()  # noqa

    def test_init_username_positional(self):
        user = User("myusername")
        self.assertEqual(user.username, "myusername")

    def test_init_username_keyword(self):
        with self.assertRaises(TypeError):
            User(username="myusername")  # noqa

    def test_init_password_default(self):
        user = User("myusername")  # noqa
        self.assertEqual(user.password, '10c8b03c7b173081a489538b2cbcf16776ef547377dd99d4737d065b1ab4c17e')

    def test_init_password_positional(self):
        with self.assertRaises(TypeError):
            User("myusername", "validpassword")  # noqa

    def test_init_password_keyword(self):
        user = User("myusername", password="validpassword")
        self.assertEqual(user.password, "37ac6a95b12ae0fb66676e1b7b47e7cffca2b957d2057c14ad8b02e7def2cea3")

    def test_init_birthday_default(self):
        user = User("myusername", password="validpassword")  # noqa
        self.assertIsNone(user.birthday)

    def test_init_birthday_positional(self):
        with self.assertRaises(TypeError):
            User("myusername", "validpassword", "2000-01-01")  # noqa

    def test_init_birthday_keyword(self):
        user = User("myusername", password="validpassword", birthday="2000-01-02")  # noqa
        self.assertIsInstance(user.birthday, date)
        self.assertEqual(user.birthday, date(2000, 1, 2))


class UserFunctionalityTest(TestCase):
    def setUp(self):
        self.user = User("myusername", password="validpassword", birthday="2000-01-02")

    def test_str(self):
        result = str(self.user)
        self.assertEqual(result, 'myusername')

    def test_repr(self):
        result = repr(self.user)
        self.assertEqual(result, "User('myusername', birthday='2000-01-02')")

    def test_sayhello(self):
        result = self.user.say_hello()
        self.assertEqual(result, 'hello')


class UserAuthenticationTest(TestCase):
    def setUp(self):
        self.user = User("myusername", password="validpassword")

    def test_authenticated_default(self):
        self.assertFalse(self.user._authenticated)

    def test_authenticated_when_logged_in(self):
        self.user._authenticated = True
        self.assertTrue(self.user.is_authenticated())

    def test_authenticated_when_logged_out(self):
        self.user._authenticated = False
        self.assertFalse(self.user.is_authenticated())

    def test_login_valid(self):
        self.user._authenticated = False
        self.user.login('validpassword')
        self.assertTrue(self.user._authenticated)

    def test_login_invalid(self):
        self.user._authenticated = False
        self.user.login('invalidpassword')
        self.assertFalse(self.user._authenticated)

    def test_logout(self):
        self.user._authenticated = True
        self.user.logout()
        self.assertFalse(self.user._authenticated)

16.2.18. Mock

  • Mock and MagicMock objects create all attributes and methods as you access them and store details of how they have been used.

from unittest.mock import MagicMock

thing = ProductionClass()
thing.method = MagicMock(return_value=3)

thing.method(3, 4, 5, key='value')
# 3

thing.method.assert_called_with(3, 4, 5, key='value')

16.2.19. Side effect

  • Raising an exception when a mock is called

from unittest.mock import Mock

mock = Mock(side_effect=KeyError('foo'))

mock()
# Traceback (most recent call last):
# KeyError: 'foo'

16.2.20. patch

  • The object you specify will be replaced with a mock (or other object) during the test and restored when the test ends

from unittest.mock import patch

@patch('module.ClassName2')
@patch('module.ClassName1')
def test(MockClass1, MockClass2):
    module.ClassName1()
    module.ClassName2()
    assert MockClass1 is module.ClassName1
    assert MockClass2 is module.ClassName2
    assert MockClass1.called
    assert MockClass2.called


test()
from unittest.mock import patch

class MyClass:
    def method(self)
        pass

with patch.object(MyClass, 'method', return_value=None) as mock_method:
    thing = MyClass()
    thing.method(1, 2, 3)

mock_method.assert_called_once_with(1, 2, 3)

16.2.21. Stub

  • writing classes or functions but not yet implementing them

  • After you have planned a module or class, for example by drawing it's UML diagram, you begin implementing it.

  • As you may have to implement a lot of methods and classes, you begin with stubs.

  • This simply means that you only write the definition of a function down and leave the actual code for later.

class Foo:
     def bar(self):
         raise NotImplementedError

     def tank(self):
         raise NotImplementedError

16.2.22. Use Case - 0x01

>>> def capture(func, *args, **kwargs):
...     """Return a tuple with the returned exception and value."""
...     try:
...         return [None, func(*args, **kwargs)]
...     except Exception as e:
...         return [type(e), None]
...
>>>
>>>
>>> for x in (-10, -1, 0, 0.2, 100, float('Inf'), float( 'Nan'), 'hello'):
...     assert capture(log, x) == capture(altlog, X)  

16.2.23. Use Case - 0x02

>>> device_status = 'Status: connected'
>>> load_limits = False
>>> load = 75
>>> dead_ports = {12, 8, 15, 25}
>>> ports_in_use = {10, 14, 6, 7, 20}
>>> allowed_ports = (2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18, 20, 22)
>>>
>>>
>>> def is_active(x):
...     """Predicate returning True if x is active port"""
...     return x % 2 == 0

Instead of:

>>> overlap = False
>>> for port in ports_in_use:
...     if port in dead_ports:
...         overlap = True
>>> assert not overlap

Write:

>>> assert not any([port in dead_ports for port in ports_in_use])
>>> assert not any(port in dead_ports for port in ports_in_use)
>>> assert not dead_ports. intersection(ports_in_use)
>>> assert dead_ports.isdisjoint(ports_in_use)

Test objective: Make sure that all the ports in use are under 1024

Bad:

>>> okay = True
>>> for port in ports_in_use:
...     if port >= 1024:
...         okay = False
>>> assert okay

Good:

>>> assert sum( [port < 1024 for port in ports_in_use]) == len(ports_in_use)
>>> assert all([port < 1024 for port in ports_in_use])
>>> assert all(port < 1024 for port in ports_in_use)

16.2.24. Assignments

Code 16.15. Solution
"""
* Assignment: DevOps Unittest Rectangle
* Complexity: medium
* Lines of code: 100 lines
* Time: 21 min

English:
    1. Write unittest for `Rectangle`
    2. Run unittest - all must succeed

Polish:
    1. Napisz testy jednostkowe dla `Rectangle`
    2. Uruchom unittest - wszystkie muszą się powieść
"""

import unittest


class Rectangle:

    def __init__(self, a, b):
        self.side_a = a
        self.side_b = b

        if a <= 0 or b <= 0:
            raise ValueError('Side length must be positive')

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

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

    def __str__(self):
        return f'Rectangle({self.side_a}, {self.side_b})'