7. Unittesting

../_images/geek-and-poke-development-driven-tests.jpg

Fig. 7.1. Development driven tests

7.1. Glossary

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.
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.
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.

7.2. Using unittest

Code Listing 7.1. Przykład pokrycia klasy za pomocą unittest.
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='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')
Code Listing 7.2. Przykład pokrycia klasy za pomocą unittest
import unittest
from typing import Union


class Prostokat:
    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('Długość boku A musi być więszka od 0')

        if not isinstance(bok_b, (float, int)) or bok_b <= 0:
            raise ValueError('Długość boku B musi być więszka od 0')

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

    def pole(self) -> float:
        return self.bok_a * self.bok_b

    def obwod(self) -> float:
        return 2 * (self.bok_a+self.bok_b)

    def __str__(self) -> str:
        return f'Prostokat(a={self.bok_a}, b={self.bok_b})'


class ProstokatTest(unittest.TestCase):

    def setUp(self):
        self.prostokat = Prostokat(bok_a=10, bok_b=20)

    def test_poprawnego_tworzenie_prostokata(self):
        Prostokat(bok_a=5, bok_b=10)

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

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

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

    def test_prostokata_bok_zero(self):
        with self.assertRaises(ValueError):
            Prostokat(bok_a=0, bok_b=20)

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

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

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

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

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

    def test_ustawienia_jednego_boku(self):
        with self.assertRaises(TypeError):
            Prostokat(bok_a=0)

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

    def test_przechowywanie_wartosci(self):
        p = Prostokat(bok_a=5, bok_b=10)
        self.assertEqual(p.bok_a, 5)
        self.assertEqual(p.bok_b, 10)

    def test_tworzenie_prostokata(self):
        self.assertEqual(self.prostokat.bok_a, 10)
        self.assertEqual(self.prostokat.bok_b, 20)

    def test_pola(self):
        self.assertEqual(self.prostokat.pole(), 200.0)

    def test_obwod(self):
        self.assertEqual(self.prostokat.obwod(), 60)

    def test_prostokat_to_string(self):
        self.assertEqual(str(self.prostokat), 'Prostokat(a=10.0, b=20.0)')


if __name__ == '__main__':
    unittest.main()
Code Listing 7.3. Przykład pokrycia klasy za pomocą 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')
Usage:
$ python -m unittest FILENAME.py

7.3. Assignments

7.3.1. Dragon

  1. Napisz testy jednostkowe dla Smoka z OOP