2.1. Unit Testing¶

Figure 2.1. Development driven tests¶
2.1.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.
2.1.2. Running tests¶
2.1.2.1. Running tests with your IDE¶
View menu -> Run... -> Unittest in
myfunction
2.1.2.2. From code¶
if __name__ == "__main__":
import unittest
unittest.main()
2.1.2.3. From command line¶
Display only errors. With -v
display progress:
$ python -m unittest myfile.py
$ python -m unittest -v myfile.py
2.1.3. Examples¶
2.1.3.1. Example 1¶
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Optional
from unittest import TestCase
@dataclass
class User:
firstname: str
lastname: 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):
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='Jan', lastname='Twardowski', date_of_birth=now)
def tearDown(self) -> None:
pass
def test_create_user(self):
user = User(firstname='Jan', lastname='Twardowski')
self.assertEqual(user.firstname, 'Jan')
self.assertEqual(user.lastname, 'Twardowski')
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='Jan', lastname='Twardowski', date_of_birth=now)
self.assertEqual(user.date_of_birth.tzinfo, timezone.utc)
def test_str(self):
self.assertEqual(str(self.user), 'User(firstname="Jan", lastname="Twardowski")')
2.1.3.2. Example 2¶
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')
2.1.3.3. Example 3¶
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)
2.1.3.4. Example 4¶
from dataclasses import dataclass
from datetime import date, datetime, timezone
from typing import Optional
from unittest import TestCase
@dataclass
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='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')
2.1.3.5. Example 5¶
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):
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()
2.1.4. 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')
2.1.4.1. 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'
2.1.4.2. 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)
2.1.5. 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
2.1.6. Assignments¶
"""
* Assignment: DevSecOps Unittest Rectangle
* Filename: devsecops_unittest_dragon.py
* Complexity: medium
* Lines of code: 100 lines
* Time: 21 min
English:
1. Write unittest for `Rectangle`
Polish:
1. Napisz testy jednostkowe dla `Rectangle`
"""
# Given
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.a}, {self.b})'
"""
* Assignment: DevSecOps Unittest Dragon
* Filename: devsecops_unittest_dragon.py
* Complexity: medium
* Lines of code: 100 lines
* Time: 21 min
English:
1. Write unittest for the dragon from :ref:`Dragon Alpha`
Polish:
1. Napisz testy jednostkowe dla Smoka z :ref:`Dragon Alpha`
"""
# Given
from random import randint
from unittest import TestCase
class Config:
RESOLUTION_X_MIN = 0
RESOLUTION_X_MAX = 1024
RESOLUTION_Y_MIN = 0
RESOLUTION_Y_MAX = 768
class Status:
ALIVE = 'alive'
DEAD = 'dead'
class Dragon:
DAMAGE_MIN = 5
DAMAGE_MAX = 20
class IsDead(Exception):
pass
def __init__(self, name, x=0, y=0):
self.status = Status.ALIVE
self.name = name
self.position_x = x
self.position_y = y
def get_position(self):
return self.position_x, self.position_y
def set_position(self, x, y):
if x > Config.RESOLUTION_X_MAX:
x = Config.RESOLUTION_X_MAX
if x < Config.RESOLUTION_X_MIN:
x = Config.RESOLUTION_X_MIN
if y > Config.RESOLUTION_Y_MAX:
y = Config.RESOLUTION_Y_MAX
if y < Config.RESOLUTION_Y_MIN:
y = Config.RESOLUTION_Y_MIN
self.position_x = x
self.position_y = y
def move(self, down=0, left=0, up=0, right=0):
x, y = self.get_position()
x += right - left
y += down - up
self.set_position(x, y)
def make_damage(self):
if self.is_dead():
raise Dragon.IsDead
return randint(self.DAMAGE_MIN, self.DAMAGE_MAX)
def is_dead(self):
if self.status == Status.DEAD:
return True
else:
return False