how to make a person with loops


I have a problem in person/tests/test_person.py

This means that if I add more people or more cases where year_of_birth is not an integer I would have to add more tests.

I want to use one test for each of the three tests for each person, and one test for every case where year_of_birth is not an integer. I can do this with for loops.

A for loop is a way to repeat the same command over an iterable (a collection of items), it is written like this

for item in collection:
    do something

preview

I have these tests by the end of the chapter

person/tests/test_person.py
 1import datetime
 2import src.person
 3import unittest
 4
 5
 6class TestPerson(unittest.TestCase):
 7
 8    people = (
 9        (
10            'jane', 'doe', 'F', 1991,
11            True, True, True, True
12        ),
13        (
14            'joe', 'blow', 'M', 1996,
15            True, False, True, False
16        ),
17        (
18            'mary', 'public', 'F', 2000,
19            False, True, False, True
20        ),
21        (
22            'john', 'smith', 'M', 1980,
23            False, False, False, False
24        ),
25    )
26
27    @staticmethod
28    def calculate_age(year_of_birth):
29        return (
30            datetime.date.today().year
31          - year_of_birth
32        )
person/tests/test_person.py
34    def test_factory_function(self):
35        for a_person in self.people:
36            with self.subTest(first_name=a_person[0]):
37                first_name = a_person[0]
38                last_name = a_person[1]
39                sex = a_person[2]
40                year_of_birth = a_person[3]
41
42                reality = src.person.factory(
43                    first_name=first_name,
44                    last_name=last_name,
45                    sex=sex,
46                    year_of_birth=year_of_birth,
47                )
48                my_expectation = (
49                    f'{first_name}, {last_name},'
50                    f' {sex}, {year_of_birth}'
51                )
52                assert reality == my_expectation
53                self.assertEqual(reality, my_expectation)
person/tests/test_person.py
55    def test_say_hello_function(self):
56        for a_person in self.people:
57            with self.subTest(first_name=a_person[0]):
58                first_name = a_person[0]
59                last_name = a_person[1]
60                year_of_birth = a_person[3]
61
62                reality = src.person.say_hello(
63                    first_name=first_name,
64                    last_name=last_name,
65                    year_of_birth=year_of_birth,
66                )
67                my_expectation = (
68                    f'Hello, my name is {first_name}'
69                    f' {last_name} and I am'
70                    f' {self.calculate_age(year_of_birth)}.'
71                )
72                assert reality == my_expectation
73                self.assertEqual(reality, my_expectation)
person/tests/test_person.py
 75    def test_person_class(self):
 76        for a_person in self.people:
 77            with self.subTest(first_name=a_person[0]):
 78                first_name = a_person[0]
 79                last_name = a_person[1]
 80                sex = a_person[2]
 81                year_of_birth = a_person[3]
 82                is_citizen = a_person[4]
 83                passed_test = a_person[5]
 84
 85                person = src.person.Person(
 86                    first_name=first_name,
 87                    last_name=last_name,
 88                    sex=sex,
 89                    year_of_birth=year_of_birth,
 90                    is_citizen=is_citizen,
 91                    passed_test=passed_test,
 92                )
 93
 94                reality = person.say_hello()
 95                my_expectation = (
 96                    f'Hello, my name is {first_name}'
 97                    f' {last_name} and I am'
 98                    f' {self.calculate_age(year_of_birth)}.'
 99                )
100                assert reality == my_expectation
101                self.assertEqual(reality, my_expectation)
102                self.assertEqual(person.can_vote(), is_citizen)
103                self.assertEqual(
104                    person.can_get_license(), passed_test
105                )
person/tests/test_person.py
107    def test_underage_citizen(self):
108        person = src.person.Person(
109            first_name='first_name',
110            last_name='last_name',
111            sex='M',
112            year_of_birth=datetime.date.today().year-17,
113            is_citizen=True,
114            passed_test=True,
115        )
116        self.assertEqual(person.can_vote(), False)
117        self.assertEqual(
118            person.can_get_license(), False
119        )
person/tests/test_person.py
121    def test_when_person_is_too_old_to_be_alive(self):
122        with self.assertRaises(ValueError):
123            src.person.Person(
124                first_name='first_name',
125                last_name='last_name',
126                sex='F',
127                year_of_birth=datetime.date.today().year-121,
128            )
person/tests/test_person.py
130    def test_when_year_of_birth_is_not_an_integer(self):
131        for year_of_birth in (
132            datetime.date.today().year-121,
133            None,
134            2026.0,
135            '2026',
136            (2026,),
137        ):
138            with self.subTest(i=year_of_birth):
139                with self.assertRaises(TypeError):
140                    src.person.Person(
141                        first_name='first_name',
142                        last_name='last_name',
143                        sex='M',
144                        year_of_birth=year_of_birth,
145                    )
146
147    def test_dir_person_class(self):

open the project

  • I open a terminal

  • I change directory to the project

    cd person
    
  • I open test_person.py from the tests folder

  • I use pytest-watcher to run the tests automatically

    uv run pytest-watcher . --now
    

    the terminal shows

    tests/test_person.py .........                  [100%]
    
    ================= 9 passed in W.XYs ==================
    

extract test_factory_function

The tests for the factory function in test_joe, test_jane, test_john and test_mary make a call to the factory function then compare the result with a string.

RED: make it fail


I add a test for the factory function to tests/test_person.py

 6class TestPerson(unittest.TestCase):
 7
 8    @staticmethod
 9    def calculate_age(year_of_birth):
10        return (
11            datetime.date.today().year
12          - year_of_birth
13        )
14
15    def test_factory_function(self):
16        people = (
17            ('joe', 'blow', 'M', 1996),
18        )
19        for person in people:
20            reality = src.person.factory(
21                first_name=person[0],
22                last_name=person[1],
23                sex=person[2],
24                year_of_birth=person[3],
25            )
26            my_expectation = None
27            self.assertEqual(reality, my_expectation)
28
29    def test_joe(self):

the terminal is my friend, and shows AssertionError

AssertionError: 'joe, blow, M, 1996' != None

GREEN: make it pass


I change my_expectation to match reality

15    def test_factory_function(self):
16        people = (
17            ('joe', 'blow', 'M', 1996),
18        )
19        for person in people:
20            reality = src.person.factory(
21                first_name=person[0],
22                last_name=person[1],
23                sex=person[2],
24                year_of_birth=person[3],
25            )
26            my_expectation = 'joe, blow, M, 1996'
27            self.assertEqual(reality, my_expectation)
28
29    def test_joe(self):

the test passes.

  • I made a tuple named people that contains a tuple for joe

    people = (
        ('joe', 'blow', 'M', 1996),
    )
    
  • I use a for loop to repeat the same commands for each item in the people tuple, in this case there is only one item - the tuple for joe

    for person in (
        ('joe', 'blow', 'M', 1996),
    ):
    
  • I use the index of each item in joe for the parameters when the test calls the factory function

    for person in people:
    ├── a_person = ('joe', 'blow', 'M', 1996)
    └── reality = src.person.factory(
            first_name=person[0],
            last_name=person[1],
            sex=person[2],
            year_of_birth=person[3],
        )
        └── src/person/__init__.py
            └── def factory(
                    first_name, last_name,
                    sex, year_of_birth,
                ):
                ├── first_name    = 'joe'
                ├── last_name     = 'blow'
                ├── sex           = 'M'
                ├── year_of_birth = 1996
                └── return (
                        f'{first_name}, {last_name},'
                        f' {sex}, {year_of_birth}'
                    )
                    return 'joe, blow, M, 1996'
    

REFACTOR: make it better


  • I add jane to the tuple of people

    15    def test_factory_function(self):
    16        people = (
    17            ('jane', 'doe', 'F', 1991),
    18            ('joe', 'blow', 'M', 1996),
    19        )
    20        for person in people:
    

    the terminal is my friend, and shows AssertionError

    AssertionError: 'jane, doe, F, 1991' != 'joe, blow, M, 1996'
    

    because the result when the factory function is called with 'jane', 'doe', 'F' and '1991' as input is 'jane, doe, F, 1991' not 'joe, blow, M, 1996'.

        for person in (
    ┌───┴── ('jane', 'doe', 'F', 1991),
           ('joe', 'blow', 'M', 1996),
       ):
    ├── a_person = ('jane', 'doe', 'F', 1991)
    └── reality = src.person.factory(
            first_name=person[0],
            last_name=person[1],
            sex=person[2],
            year_of_birth=person[3],
        )
        └── src/person/__init__.py
            └── def factory(
                    first_name, last_name,
                    sex, year_of_birth,
                ):
                ├── first_name    = 'jane'
                ├── last_name     = 'doe'
                ├── sex           = 'F'
                ├── year_of_birth = 1991
                └── return (
                        f'{first_name}, {last_name},'
                        f' {sex}, {year_of_birth}'
                    )
                    return 'jane, doe, F, 1991'
    
  • I change my_expectation to match reality for jane

    15    def test_factory_function(self):
    16        people = (
    17            ('jane', 'doe', 'F', 1991),
    18            ('joe', 'blow', 'M', 1996),
    19        )
    20        for person in people:
    21            reality = src.person.factory(
    22                first_name=person[0],
    23                last_name=person[1],
    24                sex=person[2],
    25                year_of_birth=person[3],
    26            )
    27            # my_expectation = 'joe, blow, M, 1996'
    28            my_expectation = 'jane, doe, F, 1991'
    29            self.assertEqual(reality, my_expectation)
    30
    31    def test_joe(self):
    

    the terminal is my friend, and shows AssertionError

    AssertionError: 'joe, blow, M, 1996' != 'jane, doe, F, 1991'
    

    because the result when the factory function is called with 'joe', 'blow', 'M' and '1996' as input is 'joe, blow, M, 1996' not 'jane, doe, F, 1991'. The for loop goes through each item in the people tuple one at a time.

  • I change my_expectation in test_factory_function to an f-string

    27            # my_expectation = 'joe, blow, M, 1996'
    28            # my_expectation = 'jane, doe, F, 1991'
    29            my_expectation = (
    30                f'{person[0]}, {person[1]},'
    31                f' {person[2]}, {person[3]}'
    32            )
    33            self.assertEqual(reality, my_expectation)
    34
    35    def test_joe(self):
    

    the test passes.

  • I add variables for person[0], person[1], person[2] and person[3]

    15    def test_factory_function(self):
    16        people = (
    17            ('jane', 'doe', 'F', 1991),
    18            ('joe', 'blow', 'M', 1996),
    19        )
    20        for person in people:
    21            first_name = person[0]
    22            last_name = person[1]
    23            sex = person[2]
    24            year_of_birth = person[3]
    25
    26            reality = src.person.factory(
    27                first_name=person[0],
    28                last_name=person[1],
    29                sex=person[2],
    30                year_of_birth=person[3],
    31            )
    
  • I use the variables for person[0], person[1], person[2] and person[3]

    26            reality = src.person.factory(
    27                # first_name=person[0],
    28                # last_name=person[1],
    29                # sex=person[2],
    30                # year_of_birth=person[3],
    31                first_name=first_name,
    32                last_name=last_name,
    33                sex=sex,
    34                year_of_birth=year_of_birth,
    35            )
    36            # my_expectation = 'joe, blow, M, 1996'
    37            # my_expectation = 'jane, doe, F, 1991'
    38            my_expectation = (
    39                # f'{person[0]}, {person[1]},'
    40                # f' {person[2]}, {person[3]}'
    41                f'{first_name}, {last_name},'
    42                f' {sex}, {year_of_birth}'
    43            )
    44            self.assertEqual(reality, my_expectation)
    45
    46    def test_joe(self):
    

    the test is still green.

  • I add a tuple for mary

    15    def test_factory_function(self):
    16        people = (
    17            ('jane', 'doe', 'F', 1991),
    18            ('joe', 'blow', 'M', 1996),
    19            ('mary', 'public', 'F', 2000),
    20        )
    21        for person in people:
    

    still green.

  • I add a tuple for john

    15    def test_factory_function(self):
    16        people = (
    17            ('jane', 'doe', 'F', 1991),
    18            ('joe', 'blow', 'M', 1996),
    19            ('mary', 'public', 'F', 2000),
    20            ('john', 'smith', 'M', 1980),
    21        )
    22        for person in people:
    

    green, showing that for each tuple in the people tuple, the assertion is True.

  • I add a tuple for a person with a string as the year_of_birth

    15    def test_factory_function(self):
    16        people = (
    17            ('jane', 'doe', 'F', 1991),
    18            ('joe', 'blow', 'M', 1996),
    19            ('mary', 'public', 'F', 2000),
    20            ('john', 'smith', 'M', 1980),
    21            ('first_name', 'last_name', 'F', 'a string'),
    22        )
    23        for person in people:
    

    the test is still green, because the factory function returns a string with the inputs it gets.

  • I remove the commented lines from test_factory_function

    15    def test_factory_function(self):
    16        people = (
    17            ('jane', 'doe', 'F', 1991),
    18            ('joe', 'blow', 'M', 1996),
    19            ('mary', 'public', 'F', 2000),
    20            ('john', 'smith', 'M', 1980),
    21            ('first_name', 'last_name', 'F', 'a string'),
    22        )
    23        for person in people:
    24            first_name = person[0]
    25            last_name = person[1]
    26            sex = person[2]
    27            year_of_birth = person[3]
    28
    29            reality = src.person.factory(
    30                first_name=first_name,
    31                last_name=last_name,
    32                sex=sex,
    33                year_of_birth=year_of_birth,
    34            )
    35            my_expectation = (
    36                f'{first_name}, {last_name},'
    37                f' {sex}, {year_of_birth}'
    38            )
    39            self.assertEqual(reality, my_expectation)
    40
    41    def test_joe(self):
    
  • I remove the test for the factory function from test_joe since it is now a repetition

    41    def test_joe(self):
    42        first_name = 'joe'
    43        last_name = 'blow'
    44        sex = 'M'
    45        year_of_birth = 1996
    46
    47        reality = src.person.say_hello(
    48            first_name=first_name,
    49            last_name=last_name,
    50            year_of_birth=year_of_birth,
    51        )
    52        my_expectation = (
    53            f'Hello, my name is {first_name}'
    54            f' {last_name} and I am'
    55            f' {self.calculate_age(year_of_birth)}.'
    56        )
    57        assert reality == my_expectation
    58        self.assertEqual(reality, my_expectation)
    59
    60        joe = src.person.Person(
    61            first_name=first_name,
    62            last_name=last_name,
    63            sex=sex,
    64            year_of_birth=year_of_birth,
    65        )
    66
    67        reality = joe.say_hello()
    68        assert reality == my_expectation
    69        self.assertEqual(reality, my_expectation)
    70        self.assertEqual(joe.can_vote(), True)
    71        self.assertEqual(joe.can_get_license(), False)
    72
    73    def test_jane(self):
    
  • I remove the test for the factory function from test_jane since it is now a repetition

     73    def test_jane(self):
     74        first_name = 'jane'
     75        last_name = 'doe'
     76        sex = 'F'
     77        year_of_birth = 1991
     78
     79        reality = src.person.say_hello(
     80            first_name=first_name,
     81            last_name=last_name,
     82            year_of_birth=year_of_birth,
     83        )
     84        my_expectation = (
     85            f'Hello, my name is {first_name}'
     86            f' {last_name} and I am'
     87            f' {self.calculate_age(year_of_birth)}.'
     88        )
     89        assert reality == my_expectation
     90        self.assertEqual(reality, my_expectation)
     91
     92        jane = src.person.Person(
     93            first_name=first_name,
     94            last_name=last_name,
     95            sex=sex,
     96            year_of_birth=year_of_birth,
     97            passed_test=True,
     98        )
     99
    100        reality = jane.say_hello()
    101        assert reality == my_expectation
    102        self.assertEqual(reality, my_expectation)
    103        self.assertEqual(jane.can_vote(), True)
    104        self.assertEqual(jane.can_get_license(), True)
    105
    106    def test_john(self):
    
  • I remove the test for the factory function from test_john since it is now a repetition

    106    def test_john(self):
    107        first_name = 'john'
    108        last_name = 'smith'
    109        sex = 'M'
    110        year_of_birth = 1980
    111
    112        reality = src.person.say_hello(
    113            first_name=first_name,
    114            last_name=last_name,
    115            year_of_birth=year_of_birth,
    116        )
    117        my_expectation = (
    118            f'Hello, my name is {first_name}'
    119            f' {last_name} and I am'
    120            f' {self.calculate_age(year_of_birth)}.'
    121        )
    122        assert reality == my_expectation
    123        self.assertEqual(reality, my_expectation)
    124
    125        john = src.person.Person(
    126            first_name=first_name,
    127            last_name=last_name,
    128            sex=sex,
    129            year_of_birth=year_of_birth,
    130            is_citizen=False,
    131        )
    132
    133        reality = john.say_hello()
    134        assert reality == my_expectation
    135        self.assertEqual(reality, my_expectation)
    136        self.assertEqual(john.can_vote(), False)
    137        self.assertEqual(john.can_get_license(), False)
    138
    139    def test_mary(self):
    
  • I remove the test for the factory function from test_mary since it is now a repetition

    139    def test_mary(self):
    140        first_name = 'mary'
    141        last_name = 'public'
    142        sex = 'F'
    143        year_of_birth = 2000
    144
    145        reality = src.person.say_hello(
    146            first_name=first_name,
    147            last_name=last_name,
    148            year_of_birth=year_of_birth,
    149        )
    150        my_expectation = (
    151            f'Hello, my name is {first_name}'
    152            f' {last_name} and I am'
    153            f' {self.calculate_age(year_of_birth)}.'
    154        )
    155        assert reality == my_expectation
    156        self.assertEqual(reality, my_expectation)
    157
    158        mary = src.person.Person(
    159            first_name=first_name,
    160            last_name=last_name,
    161            sex=sex,
    162            year_of_birth=year_of_birth,
    163            is_citizen=False,
    164            passed_test=True,
    165        )
    166
    167        reality = mary.say_hello()
    168        assert reality == my_expectation
    169        self.assertEqual(reality, my_expectation)
    170        self.assertEqual(mary.can_vote(), False)
    171        self.assertEqual(mary.can_get_license(), True)
    172
    173    def test_underage_citizen(self):
    
  • I add a git commit message

    git commit -am 'extract test_factory_function'
    

The for loop allows me to test any number of people with the same test. I no longer have to write one test for each person.


extract test_say_hello_function

The tests for the say_hello function in test_joe, test_jane, test_john and test_mary make a call to the say_hello function then compare the result with a string.


RED: make it fail


I add a test for the say_hello function

39            self.assertEqual(reality, my_expectation)
40
41    def test_say_hello_function(self):
42        people = (
43            ('jane', 'doe', 'F', 1991),
44            ('joe', 'blow', 'M', 1996),
45            ('mary', 'public', 'F', 2000),
46            ('john', 'smith', 'M', 1980),
47            ('first_name', 'last_name', 'F', 'a string'),
48        )
49        for person in people:
50            first_name = person[0]
51            last_name = person[1]
52            year_of_birth = person[3]
53
54            reality = src.person.say_hello(
55                first_name=first_name,
56                last_name=last_name,
57                year_of_birth=year_of_birth,
58            )
59            my_expectation = None
60            self.assertEqual(reality, my_expectation)
61
62    def test_joe(self):

the terminal is my friend, and shows AssertionError

AssertionError:
    'Hello, my name is jane doe and I am 35.'
 != None

GREEN: make it pass


  • I change my_expectation in test_say_hello_function to match the string in the terminal

    54            reality = src.person.say_hello(
    55                first_name=first_name,
    56                last_name=last_name,
    57                year_of_birth=year_of_birth,
    58            )
    59            my_expectation = (
    60                'Hello, my name is jane doe'
    61                ' and I am 35.'
    62            )
    63            self.assertEqual(reality, my_expectation)
    64
    65    def test_joe(self):
    

    the terminal is my friend, and shows AssertionError

    AssertionError:
        'Hello, my name is joe blow and I am 30.'
     != 'Hello, my name is jane doe and I am 35.'
    

    the test passed for jane but fails for joe.

  • I change my_expectation to an f-string in test_say_hello_function

    54            reality = src.person.say_hello(
    55                first_name=first_name,
    56                last_name=last_name,
    57                year_of_birth=year_of_birth,
    58            )
    59            # my_expectation = (
    60            #     'Hello, my name is jane doe'
    61            #     ' and I am 35.'
    62            # )
    63            my_expectation = (
    64                f'Hello, my name is {first_name}'
    65                f' {last_name} and I am'
    66                f' {self.calculate_age(year_of_birth)}.'
    67            )
    68            self.assertEqual(reality, my_expectation)
    69
    70    def test_joe(self):
    

    the terminal is my friend, and shows TypeError

    E           TypeError
    

    because the say_hello function calls the calculate_age function which raises TypeError when year_of_birth is not an integer

        for person in (
           ('jane', 'doe', 'F', 1991),
           ('joe', 'blow', 'M', 1996),
           ('mary', 'public', 'F', 2000),
           ('john', 'smith', 'M', 1980),
    ┌───┴── ('first_name', 'last_name', 'F', 'a string'),
       ):
    ├── a_person = ('first_name', 'last_name', 'F', 'a string')
    ├── first_name = person[0]
    ├── last_name = person[1]
    ├── year_of_birth = person[3]
    └── reality = src.person.say_hello(
            first_name=first_name,
            last_name=last_name,
            year_of_birth=year_of_birth,
        )
        └── src/person/__init__.py
            └── def say_hello(
                    first_name, last_name, year_of_birth,
                ):
                ├── first_name    = 'first_name'
                ├── last_name     = 'last_name'
                ├── year_of_birth = 'a string'
                └── return (
                    ├── f'Hello, my name is {first_name}'
                    ├── f' {last_name} and I am'
                    └──  f' {calculate_age(year_of_birth)}.'
                    )                       └── def calculate_age(year_of_birth):
                            ├── year_of_birth = 'a string'
                            └── if not isinstance(
                                      year_of_birth, int
                                ):
                                └── raise TypeError
                                ...
    
  • I open __init__.py from the person folder in the src folder

  • I add a message to the calculate_age function for when TypeError is raised, in src/person/__init__.py

    44def calculate_age(year_of_birth):
    45    if not isinstance(year_of_birth, int):
    46        raise TypeError(
    47            f"'{year_of_birth}' is not an integer"
    48        )
    

    the terminal is my friend, and shows TypeError

    TypeError: 'a string' is not an integer
    

    better.

  • I add a try statement to test_say_hello_function in tests/test_person.py

    49        for person in people:
    50            first_name = person[0]
    51            last_name = person[1]
    52            year_of_birth = person[3]
    53
    54            try:
    55                reality = src.person.say_hello(
    56                    first_name=first_name,
    57                    last_name=last_name,
    58                    year_of_birth=year_of_birth,
    59                )
    60            except TypeError:
    61                pass
    62
    63            # my_expectation = (
    64            #     'Hello, my name is jane doe'
    65            #     ' and I am 35.'
    66            # )
    

    the terminal is my friend, and shows TypeError

    TypeError: unsupported operand type(s) for -: 'int' and 'str'
    

    because my_expectation in test_say_hello_function makes a call to the calculate_age method of TestPerson which raises TypeError because I cannot do subtraction with a string and a number.

        for person in (
           ('jane', 'doe', 'F', 1991),
           ('joe', 'blow', 'M', 1996),
           ('mary', 'public', 'F', 2000),
           ('john', 'smith', 'M', 1980),
    ┌───┴── ('first_name', 'last_name', 'F', 'a string'),
       ):
    ├── a_person = ('first_name', 'last_name', 'F', 'a string')
    ├── first_name = person[0]
    ├── last_name = person[1]
    ├── year_of_birth = person[3]
       ...
    └── my_expectation = (
        ├── f'Hello, my name is {first_name}'
        ├── f' {last_name} and I am'
        └── f' {self.calculate_age(year_of_birth)}.'
        )              @staticmethod
            └── def calculate_age(year_of_birth):
                ├── year_of_birth = 'a string'
                └── return (
                        datetime.date.today().year
                      - year_of_birth
                    )
    
  • I add an else clause to test_say_hello_function so that it only runs the assertion if the call to the say_hello function does not raise TypeError

    54            try:
    55                reality = src.person.say_hello(
    56                    first_name=first_name,
    57                    last_name=last_name,
    58                    year_of_birth=year_of_birth,
    59                )
    60            except TypeError:
    61                pass
    62            else:
    63                my_expectation = (
    64                    f'Hello, my name is {first_name}'
    65                    f' {last_name} and I am'
    66                    f' {self.calculate_age(year_of_birth)}.'
    67                )
    68                self.assertEqual(reality, my_expectation)
    69
    70            # my_expectation = (
    71            #     'Hello, my name is jane doe'
    72            #     ' and I am 35.'
    73            # )
    74
    75    def test_joe(self):
    

    the test passes.


REFACTOR: make it better


  • I want to test the error message to make sure test_say_hello_function only catches TypeError with this specific message. I can use the Exception in the except block as an object.

    54            try:
    55                reality = src.person.say_hello(
    56                    first_name=first_name,
    57                    last_name=last_name,
    58                    year_of_birth=year_of_birth,
    59                )
    60            except TypeError as error:
    61                pass
    62            else:
    

    the test is still green

  • I add assertEqual to the except block with the dir built-in function to see the attributes and methods of the Exception in the except block

    60            except TypeError as error:
    61                self.assertEqual(dir(error), [])
    62            else:
    

    the terminal is my friend, and shows AssertionError with the list of attributes and methods. Three of the names stand out because they do not have double underscores (__) before and after - add_note, args and with_traceback.

  • I change the assertion to see what is in add_note

    60            except TypeError as error:
    61                # self.assertEqual(dir(error), [])
    62                self.assertEqual(error.add_note, None)
    63            else:
    

    the terminal is my friend, and shows AssertionError

    AssertionError:
        <built-in method add_note
          of TypeError object at 0xffff7ed65c43>
     != None
    

    because add_note is a method.

  • I call the add_note method

    60            except TypeError as error:
    61                # self.assertEqual(dir(error), [])
    62                self.assertEqual(error.add_note(), None)
    63            else:
    

    the terminal is my friend, and shows TypeError

    TypeError: BaseException.add_note() takes
               exactly one argument (0 given)
    

    this is not what I want, on to the next one.

  • I change the assertion to see what is in args

    60            except TypeError as error:
    61                # self.assertEqual(dir(error), [])
    62                # self.assertEqual(error.add_note(), None)
    63                self.assertEqual(error.args, None)
    64            else:
    

    the terminal is my friend, and shows AssertionError

    AssertionError: ("'a string' is not an integer",) != None
    

    fantastic! args is a tuple that has the error message as its first and only item.

  • I use the index of the error message with an f-string

    60            except TypeError as error:
    61                # self.assertEqual(dir(error), [])
    62                # self.assertEqual(error.add_note(), None)
    63                self.assertEqual(
    64                    error.args[0],
    65                    f"'{year_of_birth}' is not an integer"
    66                )
    67            else:
    

    the test passes.

  • I change the message for TypeError in the calculate_age function in src/person/__init__.py to test my change

    44def calculate_age(year_of_birth):
    45    if not isinstance(year_of_birth, int):
    46        raise TypeError('BOOM!!!')
    47        raise TypeError(
    48            f"'{year_of_birth}' is not an integer"
    49        )
    

    the terminal is my friend, and shows AssertionError

    AssertionError: 'BOOM!!!' != "'a string' is not an integer"
    
  • I undo the change

    44def calculate_age(year_of_birth):
    45    if not isinstance(year_of_birth, int):
    46        raise TypeError(
    47            f"'{year_of_birth}' is not an integer"
    48        )
    49
    50    age = (
    51        datetime.date.today().year
    52      - year_of_birth
    53    )
    54
    55    if age > 120:
    56        raise ValueError
    57    return age
    58
    59
    60def say_hello(
    61    first_name, last_name, year_of_birth,
    62):
    

    the test is green again.

  • I remove the commented lines from test_say_hello_function in tests/test_person.py

        def test_say_hello_function(self):
            people = (
                ('jane', 'doe', 'F', 1991),
                ('joe', 'blow', 'M', 1996),
                ('mary', 'public', 'F', 2000),
                ('john', 'smith', 'M', 1980),
                ('first_name', 'last_name', 'F', 'a string'),
            )
            for person in people:
                first_name = person[0]
                last_name = person[1]
                year_of_birth = person[3]
    
                try:
                    reality = src.person.say_hello(
                        first_name=first_name,
                        last_name=last_name,
                        year_of_birth=year_of_birth,
                    )
                except TypeError as error:
                    self.assertEqual(
                        error.args[0],
                        f"'{year_of_birth}' is not an integer"
                    )
                else:
                    my_expectation = (
                        f'Hello, my name is {first_name}'
                        f' {last_name} and I am'
                        f' {self.calculate_age(year_of_birth)}.'
                    )
                    self.assertEqual(reality, my_expectation)
    
        def test_joe(self):
    
  • I remove the test for the say_hello function from test_joe since it is now a repetition, and move my_expectation below reality in the test for the say_hello method

    73    def test_joe(self):
    74        first_name = 'joe'
    75        last_name = 'blow'
    76        sex = 'M'
    77        year_of_birth = 1996
    78
    79        joe = src.person.Person(
    80            first_name=first_name,
    81            last_name=last_name,
    82            sex=sex,
    83            year_of_birth=year_of_birth,
    84        )
    85
    86        reality = joe.say_hello()
    87        my_expectation = (
    88            f'Hello, my name is {first_name}'
    89            f' {last_name} and I am'
    90            f' {self.calculate_age(year_of_birth)}.'
    91        )
    92        assert reality == my_expectation
    93        self.assertEqual(reality, my_expectation)
    94        self.assertEqual(joe.can_vote(), True)
    95        self.assertEqual(joe.can_get_license(), False)
    96
    97    def test_jane(self):
    
  • I remove the test for the say_hello function from test_jane since it is now a repetition, and move my_expectation below reality in the test for the say_hello method

     97    def test_jane(self):
     98        first_name = 'jane'
     99        last_name = 'doe'
    100        sex = 'F'
    101        year_of_birth = 1991
    102
    103        jane = src.person.Person(
    104            first_name=first_name,
    105            last_name=last_name,
    106            sex=sex,
    107            year_of_birth=year_of_birth,
    108            passed_test=True,
    109        )
    110
    111        reality = jane.say_hello()
    112        my_expectation = (
    113            f'Hello, my name is {first_name}'
    114            f' {last_name} and I am'
    115            f' {self.calculate_age(year_of_birth)}.'
    116        )
    117        assert reality == my_expectation
    118        self.assertEqual(reality, my_expectation)
    119        self.assertEqual(jane.can_vote(), True)
    120        self.assertEqual(jane.can_get_license(), True)
    121
    122    def test_john(self):
    
  • I remove the test for the say_hello function from test_john since it is now a repetition, and move my_expectation below reality in the test for the say_hello method

    122    def test_john(self):
    123        first_name = 'john'
    124        last_name = 'smith'
    125        sex = 'M'
    126        year_of_birth = 1980
    127
    128        john = src.person.Person(
    129            first_name=first_name,
    130            last_name=last_name,
    131            sex=sex,
    132            year_of_birth=year_of_birth,
    133            is_citizen=False,
    134        )
    135
    136        reality = john.say_hello()
    137        my_expectation = (
    138            f'Hello, my name is {first_name}'
    139            f' {last_name} and I am'
    140            f' {self.calculate_age(year_of_birth)}.'
    141        )
    142        assert reality == my_expectation
    143        self.assertEqual(reality, my_expectation)
    144        self.assertEqual(john.can_vote(), False)
    145        self.assertEqual(john.can_get_license(), False)
    146
    147    def test_mary(self):
    
  • I remove the test for the say_hello function from test_mary since it is now a repetition, and move my_expectation below reality in the test for the say_hello method

    147    def test_mary(self):
    148        first_name = 'mary'
    149        last_name = 'public'
    150        sex = 'F'
    151        year_of_birth = 2000
    152
    153        mary = src.person.Person(
    154            first_name=first_name,
    155            last_name=last_name,
    156            sex=sex,
    157            year_of_birth=year_of_birth,
    158            is_citizen=False,
    159            passed_test=True,
    160        )
    161
    162        reality = mary.say_hello()
    163        my_expectation = (
    164            f'Hello, my name is {first_name}'
    165            f' {last_name} and I am'
    166            f' {self.calculate_age(year_of_birth)}.'
    167        )
    168        assert reality == my_expectation
    169        self.assertEqual(reality, my_expectation)
    170        self.assertEqual(mary.can_vote(), False)
    171        self.assertEqual(mary.can_get_license(), True)
    172
    173    def test_underage_citizen(self):
    
  • I add a git commit message

    git commit -am 'extract test_say_hello_function'
    

For each person in the people tuple, this test calls the say_hello function

  • If the call raises TypeError, it asserts that the error message is correct

    • If the error message is not correct it raises AssertionError

          for person in (
             ('jane', 'doe', 'F', 1991),
             ('joe', 'blow', 'M', 1996),
             ('mary', 'public', 'F', 2000),
             ('john', 'smith', 'M', 1980),
      ┌───┴── ('first_name', 'last_name', 'F', 'a string'),
         ):
      ├── a_person = ('first_name', 'last_name', 'F', 'a string')
      ├── first_name = person[0]
      ├── last_name = person[1]
      ├── year_of_birth = person[3]
      └── try:
              reality = src.person.say_hello(
                  first_name=first_name,
                  last_name=last_name,
                  year_of_birth=year_of_birth,
              )
              └── src/person/__init__.py
                  └── def say_hello(
                          first_name, last_name, year_of_birth,
                      ):
                      ├── first_name    = 'first_name'
                      ├── last_name     = 'last_name'
                      ├── year_of_birth = 'a string'
                      └── return (
                          ├── f'Hello, my name is {first_name}'
                          ├── f' {last_name} and I am'
                          └── f' {calculate_age(year_of_birth)}.'
                          )                           └── def calculate_age(year_of_birth):
                                  ├── year_of_birth = 'a string'
                                  └── if not isinstance(
                                          year_of_birth, int
                                      ):
      ┌───────────────────────────────┴── raise TypeError(
                                             'BOOM!!!'
                                         )
      └── except TypeError as error:
          └── self.assertEqual(
                  error.args[0],
                  f"'{year_of_birth}' is not an integer"
              )
              └── raise AssertionError
          else:
              ...
      
    • If the error message is correct the test passes

          for person in (
             ('jane', 'doe', 'F', 1991),
             ('joe', 'blow', 'M', 1996),
             ('mary', 'public', 'F', 2000),
             ('john', 'smith', 'M', 1980),
      ┌───┴── ('first_name', 'last_name', 'F', 'a string'),
         ):
      ├── a_person = ('first_name', 'last_name', 'F', 'a string')
      ├── first_name = person[0]
      ├── last_name = person[1]
      ├── year_of_birth = person[3]
      └── try:
              reality = src.person.say_hello(
                  first_name=first_name,
                  last_name=last_name,
                  year_of_birth=year_of_birth,
              )
              └── src/person/__init__.py
                  └── def say_hello(
                          first_name, last_name, year_of_birth,
                      ):
                      ├── first_name    = 'first_name'
                      ├── last_name     = 'last_name'
                      ├── year_of_birth = 'a string'
                      └── return (
                          ├── f'Hello, my name is {first_name}'
                          ├── f' {last_name} and I am'
                          └── f' {calculate_age(year_of_birth)}.'
                          )                           └── def calculate_age(year_of_birth):
                                  ├── year_of_birth = 'a string'
                                  └── if not isinstance(
                                          year_of_birth, int
                                      ):
      ┌───────────────────────────────┴── raise TypeError(
                                             f"'{year_of_birth}'"
                                             " is not an integer"
                                         )
      └── except TypeError as error:
          └── self.assertEqual(
                  error.args[0],
                  f"'{year_of_birth}' is not an integer"
              )
          else:
              ...
      
  • If the call to the say_hello function does not raise TypeError, it asserts that the result of the call matches the expectation

        for person in (
           ('jane', 'doe', 'F', 1991),
           ('joe', 'blow', 'M', 1996),
           ('mary', 'public', 'F', 2000),
    ┌───┴── ('john', 'smith', 'M', 1980),
           ('first_name', 'last_name', 'F', 'a string'),
       ):
    ├── a_person = ('john', 'smith', 'M', 1980),
    ├── first_name = person[0]
    ├── last_name = person[1]
    ├── year_of_birth = person[3]
    ├── try:
           reality = src.person.say_hello(
               first_name=first_name,
               last_name=last_name,
               year_of_birth=year_of_birth,
           )
           └── src/person/__init__.py
               └── def say_hello(
                       first_name, last_name, year_of_birth,
                   ):
                   ├── first_name    = 'john'
                   ├── last_name     = 'smith'
                   ├── year_of_birth = 1980
                   └── return (
                       ├── f'Hello, my name is {first_name}'
                       ├── f' {last_name} and I am'
                       └── f' {calculate_age(year_of_birth)}.'
                       )                          └── def calculate_age(year_of_birth):
                               ├── ...
                               └── return age
       except TypeError as error:
           ...
    └── else:
        ├── my_expectation = (
           ├── f'Hello, my name is {first_name}'
           ├── f' {last_name} and I am'
           └── f' {self.calculate_age(year_of_birth)}.'
           )                 @staticmethod
               └── def calculate_age(year_of_birth):
                   ├── year_of_birth = 2000
                   └── return (
                           datetime.date.today().year
                         - year_of_birth
                       )
        └── self.assertEqual(reality, my_expectation)
    

extract test_say_hello_method

The tests in test_joe, test_jane, test_john and test_mary make an instance of the Person class, then call its say_hello comparing the results in assertions.


RED: make it fail


I add a test for the Person class

71                self.assertEqual(reality, my_expectation)
72
73    def test_say_hello_method(self):
74        people = (
75            ('jane', 'doe', 'F', 1991),
76            ('joe', 'blow', 'M', 1996),
77            ('mary', 'public', 'F', 2000),
78            ('john', 'smith', 'M', 1980),
79            ('first_name', 'last_name', 'F', 'a string'),
80        )
81        for person in people:
82            first_name = person[0]
83            last_name = person[1]
84            sex = person[2]
85            year_of_birth = person[3]
86
87            a_person = src.person.Person(
88                first_name=first_name,
89                last_name=last_name,
90                sex=sex,
91                year_of_birth=year_of_birth,
92            )
93
94            self.assertEqual(
95                a_person.say_hello(),
96                None
97            )
98
99    def test_joe(self):

the terminal is my friend, and shows AssertionError

AssertionError:
    'Hello, my name is jane doe and I am 35.'
  != None

GREEN: make it pass


  • I change the expectation of the assertion to match the string from the terminal

    94            self.assertEqual(
    95                a_person.say_hello(),
    96                'Hello, my name is jane doe and I am 35.'
    97            )
    98
    99    def test_joe(self):
    

    the terminal is my friend, and shows AssertionError

    AssertionError:
        'Hello, my name is joe blow and I am 30.'
     != 'Hello, my name is jane doe and I am 35.'
    
  • I change the expectation of the assertion to an f-string

     94            self.assertEqual(
     95                a_person.say_hello(),
     96                (
     97                    f'Hello, my name is {first_name}'
     98                    f' {last_name} and I am'
     99                    f' {self.calculate_age(year_of_birth)}.'
    100                )
    101            )
    102
    103    def test_joe(self):
    

    the terminal is my friend, and shows TypeError

    TypeError: 'a string' is not an integer
    

    this is the correct Exception for when the year_of_birth is not an integer. I need a better way to know which item in the people tuple raised the Exception.


the subTest method

unittest.TestCase has a method that I can use to show what items in a loop cause a failure in a test. It gives me a way to name each sub test in each loop.

  • I add a call to the unittest.TestCase.subTest method

     81        for person in people:
     82            first_name = person[0]
     83            last_name = person[1]
     84            sex = person[2]
     85            year_of_birth = person[3]
     86
     87            with self.subTest(first_name=first_name):
     88                a_person = src.person.Person(
     89                    first_name=first_name,
     90                    last_name=last_name,
     91                    sex=sex,
     92                    year_of_birth=year_of_birth,
     93                )
     94                self.assertEqual(
     95                    a_person.say_hello(),
     96                    (
     97                        f'Hello, my name is {first_name}'
     98                        f' {last_name} and I am'
     99                        f' {self.calculate_age(year_of_birth)}.'
    100                    )
    101                )
    102
    103    def test_joe(self):
    

    the terminal is my friend, and shows TypeError

    SUBFAILED(first_name='first_name') ...test_say_hello_method -
        TypeError: 'a string' is not an integer
    

    SUBFAILED(first_name='first_name') shows the value I gave in with self.subTest(first_name=first_name) as the label for the sub test. I can use any name and values I want.

  • I add year_of_birth to the call to the subTest method

    81        for person in people:
    82            first_name = person[0]
    83            last_name = person[1]
    84            sex = person[2]
    85            year_of_birth = person[3]
    86
    87            with self.subTest(
    88                first_name=first_name,
    89                year_of_birth=year_of_birth,
    90            ):
    91                a_person = src.person.Person(
    92                    first_name=first_name,
    93                    last_name=last_name,
    94                    sex=sex,
    95                    year_of_birth=year_of_birth,
    96                )
    

    the terminal still shows AssertionError with the extra name and value.

    SUBFAILED(first_name='first_name', year_of_birth='a string') ...
        test_say_hello_method - TypeError: 'a string' is not an integer
    
  • I add a try statement for TypeError and this error message to test_say_hello_method

     73    def test_say_hello_method(self):
     74        people = (
     75            ('jane', 'doe', 'F', 1991),
     76            ('joe', 'blow', 'M', 1996),
     77            ('mary', 'public', 'F', 2000),
     78            ('john', 'smith', 'M', 1980),
     79            ('first_name', 'last_name', 'F', 'a string'),
     80        )
     81        for person in people:
     82            first_name = person[0]
     83            last_name = person[1]
     84            sex = person[2]
     85            year_of_birth = person[3]
     86
     87            with self.subTest(
     88                first_name=first_name,
     89                year_of_birth=year_of_birth,
     90            ):
     91                try:
     92                    a_person = src.person.Person(
     93                        first_name=first_name,
     94                        last_name=last_name,
     95                        sex=sex,
     96                        year_of_birth=year_of_birth,
     97                    )
     98                except TypeError as error:
     99                    self.assertEqual(
    100                        error.args[0],
    101                        f"'{year_of_birth}' is not an integer"
    102                    )
    103                else:
    104                    self.assertEqual(
    105                        a_person.say_hello(),
    106                        (
    107                            f'Hello, my name is {first_name}'
    108                            f' {last_name} and I am'
    109                            f' {self.calculate_age(year_of_birth)}.'
    110                        )
    111                    )
    112
    113    def test_joe(self):
    

    the test passes.


REFACTOR: make it better


  • I change the error message for if the value of the year_of_birth parameter is not an integer in the calculate_age function, in src/person/__init__.py

    44def calculate_age(year_of_birth):
    45    if not isinstance(year_of_birth, int):
    46        raise TypeError('BOOM')
    47        raise TypeError(
    48            f"'{year_of_birth}' is not an integer"
    49        )
    

    the terminal is my friend, and shows ASsertionError

    SUBFAILED(first_name='first_name', year_of_birth='a string') ...
        test_say_hello_method - AssertionError:
            'BOOM' != "'a string' is not an integer"
    FAILED tests/test_person.py...
        test_say_hello_function - AssertionError:
            'BOOM' != "'a string' is not an integer"
    
  • I want test_say_hello_function to also show which person raises an Exception. I add a call to the subTest method from test_say_hello_function in tests/test_person.py

    41    def test_say_hello_function(self):
    42        people = (
    43            ('jane', 'doe', 'F', 1991),
    44            ('joe', 'blow', 'M', 1996),
    45            ('mary', 'public', 'F', 2000),
    46            ('john', 'smith', 'M', 1980),
    47            ('first_name', 'last_name', 'F', 'a string'),
    48        )
    49        for person in people:
    50            first_name = person[0]
    51            last_name = person[1]
    52            year_of_birth = person[3]
    53
    54            with self.subTest(
    55                first_name=first_name,
    56                year_of_birth=year_of_birth,
    57            ):
    58                try:
    59                    reality = src.person.say_hello(
    60                        first_name=first_name,
    61                        last_name=last_name,
    62                        year_of_birth=year_of_birth,
    63                    )
    64                except TypeError as error:
    65                    self.assertEqual(
    66                        error.args[0],
    67                        f"'{year_of_birth}' is not an integer"
    68                    )
    69                else:
    70                    my_expectation = (
    71                        f'Hello, my name is {first_name}'
    72                        f' {last_name} and I am'
    73                        f' {self.calculate_age(year_of_birth)}.'
    74                    )
    75                    self.assertEqual(reality, my_expectation)
    76
    77    def test_say_hello_method(self):
    

    the terminal shows AssertionError with the first name and year_of_birth of the person that raised the Exception

    SUBFAILED(first_name='first_name', year_of_birth='a string') ...
        test_say_hello_method - AssertionError:
            'BOOM' != "'a string' is not an integer"
    SUBFAILED(first_name='first_name', year_of_birth='a string') ...
        test_say_hello_function - AssertionError:
            'BOOM' != "'a string' is not an integer"
    
  • I change the error message in in the calculate_age function if the value of the year_of_birth parameter is not an integer back to the correct message, in src/person/__init__.py

    44def calculate_age(year_of_birth):
    45    if not isinstance(year_of_birth, int):
    46        raise TypeError(
    47            f"'{year_of_birth}' is not an integer"
    48        )
    49    ...
    

    the tests are green again.

  • I remove the assertions for the say_hello method from test_joe

    104    def test_joe(self):
    105        first_name = 'joe'
    106        last_name = 'blow'
    107        sex = 'M'
    108        year_of_birth = 1996
    109
    110        joe = src.person.Person(
    111            first_name=first_name,
    112            last_name=last_name,
    113            sex=sex,
    114            year_of_birth=year_of_birth,
    115        )
    116        self.assertEqual(joe.can_vote(), True)
    117        self.assertEqual(joe.can_get_license(), False)
    118
    119    def test_jane(self):
    
  • I remove the assertions for the say_hello method from test_jane

    132    def test_jane(self):
    133        first_name = 'jane'
    134        last_name = 'doe'
    135        sex = 'F'
    136        year_of_birth = 1991
    137
    138        jane = src.person.Person(
    139            first_name=first_name,
    140            last_name=last_name,
    141            sex=sex,
    142            year_of_birth=year_of_birth,
    143            passed_test=True,
    144        )
    145        self.assertEqual(jane.can_vote(), True)
    146        self.assertEqual(jane.can_get_license(), True)
    147
    148    def test_john(self):
    
  • I remove the assertions for the say_hello method from test_john

    148    def test_john(self):
    149        first_name = 'john'
    150        last_name = 'smith'
    151        sex = 'M'
    152        year_of_birth = 1980
    153
    154        john = src.person.Person(
    155            first_name=first_name,
    156            last_name=last_name,
    157            sex=sex,
    158            year_of_birth=year_of_birth,
    159            is_citizen=False,
    160        )
    161        self.assertEqual(john.can_vote(), False)
    162        self.assertEqual(john.can_get_license(), False)
    163
    164    def test_mary(self):
    
  • I remove the assertions for the say_hello method from test_mary

    164    def test_mary(self):
    165        first_name = 'mary'
    166        last_name = 'public'
    167        sex = 'F'
    168        year_of_birth = 2000
    169
    170        mary = src.person.Person(
    171            first_name=first_name,
    172            last_name=last_name,
    173            sex=sex,
    174            year_of_birth=year_of_birth,
    175            is_citizen=False,
    176            passed_test=True,
    177        )
    178        self.assertEqual(mary.can_vote(), False)
    179        self.assertEqual(mary.can_get_license(), True)
    180
    181    def test_underage_citizen(self):
    
  • I add a git commit message

    git commit -am 'extract test_say_hello_method'
    

For each person in the people tuple, this test makes an instance of the Person class which makes an age attribute by calling the calculate_age with the given year_of_birth parameter.

  • If the call raises TypeError, it asserts that the error message is correct

    • If the error message is not correct it raises AssertionError

          for person in (
             ('jane', 'doe', 'F', 1991),
             ('joe', 'blow', 'M', 1996),
             ('mary', 'public', 'F', 2000),
             ('john', 'smith', 'M', 1980),
      ┌───┴── ('first_name', 'last_name', 'F', 'a string'),
         ):
      ├── a_person = ('first_name', 'last_name', 'F', 'a string')
      ├── first_name = person[0]
      ├── last_name = person[1]
      ├── sex = person[2]
      ├── year_of_birth = person[3]
      └── with self.subTest(
              first_name=first_name,
              year_of_birth=year_of_birth,
          ):
          └── try:
              └── a_person = src.person.Person(
                      first_name=first_name,
                      last_name=last_name,
                      sex=sex,
                      year_of_birth=year_of_birth,
                  )
                  └── src/person/__init__.py
                      └── class Person:
                          └── def __init__(
                                  self, first_name, last_name,
                                  sex, year_of_birth=None,
                                  is_citizen=True,
                                  passed_test=False,
                              ):
                              ├── self.first_name = first_name
                              ├── self.last_name = last_name
                              ├── self.year_of_birth = year_of_birth
                              ├── self.sex = sex
                              ├── self.is_citizen = is_citizen
                              ├── self.passed_test = passed_test
                              └── self.age = calculate_age(
                                      year_of_birth
                                  )
                                  └── def calculate_age(
                                          year_of_birth
                                      ):
                                      ├── year_of_birth = 'a string'
                                      └── if not isinstance(
                                              year_of_birth, int
                                          ):
          ┌───────────────────────────────┴── raise TypeError(
                                                 'BOOM!!!'
                                             )
          └── except TypeError as error:
              └── self.assertEqual(
                      error.args[0],
                      f"'{year_of_birth}' is not an integer"
                  )
                  └── raise AssertionError
              else:
                  ...
      
    • If the error message is correct the test passes

          for person in (
             ('jane', 'doe', 'F', 1991),
             ('joe', 'blow', 'M', 1996),
             ('mary', 'public', 'F', 2000),
             ('john', 'smith', 'M', 1980),
      ┌───┴── ('first_name', 'last_name', 'F', 'a string'),
         ):
      ├── a_person = ('first_name', 'last_name', 'F', 'a string')
      ├── first_name = person[0]
      ├── last_name = person[1]
      ├── sex = person[2]
      ├── year_of_birth = person[3]
      └── with self.subTest(
              first_name=first_name,
              year_of_birth=year_of_birth,
          ):
          └── try:
              └── a_person = src.person.Person(
                      first_name=first_name,
                      last_name=last_name,
                      sex=sex,
                      year_of_birth=year_of_birth,
                  )
                  └── src/person/__init__.py
                      └── class Person:
                          └── def __init__(
                                  self, first_name, last_name,
                                  sex, year_of_birth=None,
                                  is_citizen=True,
                                  passed_test=False,
                              ):
                              ├── self.first_name = first_name
                              ├── self.last_name = last_name
                              ├── self.year_of_birth = year_of_birth
                              ├── self.sex = sex
                              ├── self.is_citizen = is_citizen
                              ├── self.passed_test = passed_test
                              └── self.age = calculate_age(
                                      year_of_birth
                                  )
                                  └── def calculate_age(
                                          year_of_birth
                                      ):
                                      ├── year_of_birth = 'a string'
                                      └── if not isinstance(
                                              year_of_birth, int
                                          ):
          ┌───────────────────────────────┴── raise TypeError(
                                                 f"'{year_of_birth}'"
                                                 " is not an integer"
                                             )
          └── except TypeError as error:
              └── self.assertEqual(
                      error.args[0],
                      f"'{year_of_birth}' is not an integer"
                  )
              else:
                  ...
      
  • If the call to the calculate_age function does not raise TypeError, it asserts that the result of the call to the say_hello method matches the expectation

        for person in (
           ('jane', 'doe', 'F', 1991),
    ┌───┴── ('joe', 'blow', 'M', 1996),
           ('mary', 'public', 'F', 2000),
           ('john', 'smith', 'M', 1980),
           ('first_name', 'last_name', 'F', 'a string'),
       ):
    ├── a_person = ('first_name', 'last_name', 'F', 'a string')
    ├── first_name = person[0]
    ├── last_name = person[1]
    ├── sex = person[2]
    ├── year_of_birth = person[3]
    └── with self.subTest(
            first_name=first_name,
            year_of_birth=year_of_birth,
        ):
        ├── try:
           └── a_person = src.person.Person(
                   first_name=first_name,
                   last_name=last_name,
                   sex=sex,
                   year_of_birth=year_of_birth,
               )
               └── src/person/__init__.py
                   └── class Person:
                       └── def __init__(
                               self, first_name, last_name,
                               sex, year_of_birth=None,
                               is_citizen=True,
                               passed_test=False,
                           ):
                           ├── self.first_name = first_name
                           ├── self.last_name = last_name
                           ├── self.year_of_birth = year_of_birth
                           ├── self.sex = sex
                           ├── self.is_citizen = is_citizen
                           ├── self.passed_test = passed_test
                           └── self.age = calculate_age(
                                   year_of_birth
                               )
                               └── def calculate_age(
                                       year_of_birth
                                   ):
                                   ├── year_of_birth = 1996
                                   ├── ...
                                   └── return age
           except TypeError as error:
               ...
        └── else:
            └── self.assertEqual(
                ├── a_person.say_hello(),
                   └── src/person/__init__.py
                       └── class Person:
                              ...
                           └── def say_hello(self):
                               └── return (
                                   ├── 'Hello, my name is'
                                   ├── f' {self.first_name}'
                                   ├── f' {self.last_name} and I am'
                                   └── f' {self.age}.'
                                   )
                └── (
                    ├── f'Hello, my name is {first_name}'
                    ├── f' {last_name} and I am'
                    └── f' {self.calculate_age(year_of_birth)}.'
                    )               )          @staticmethod
                        └── def calculate_age(year_of_birth):
                            ├── year_of_birth = 1996
                            └── return (
                                    datetime.date.today().year
                                  - year_of_birth
                                )
    

extract people class attribute

test_factory_function, test_say_hello_function and test_say_hello_method all use the same tuple of persons.

  • I add a class attribute for the people tuple to the TestPerson class

     6class TestPerson(unittest.TestCase):
     7
     8    people = (
     9        ('jane', 'doe', 'F', 1991),
    10        ('joe', 'blow', 'M', 1996),
    11        ('mary', 'public', 'F', 2000),
    12        ('john', 'smith', 'M', 1980),
    13        ('first_name', 'last_name', 'F', 'a string'),
    14    )
    15
    16    @staticmethod
    17    def calculate_age(year_of_birth):
    
  • I use the class attribute for people in the for loop in test_factory_function

    23    def test_factory_function(self):
    24        # people = (
    25        #     ('jane', 'doe', 'F', 1991),
    26        #     ('joe', 'blow', 'M', 1996),
    27        #     ('mary', 'public', 'F', 2000),
    28        #     ('john', 'smith', 'M', 1980),
    29        #     ('first_name', 'last_name', 'F', '2026'),
    30        # )
    31        # for person in people:
    32        for person in self.people:
    33            first_name = person[0]
    

    the test is still green.

  • I remove the commented lines from test_factory_function

    23    def test_factory_function(self):
    24        for person in self.people:
    25            first_name = person[0]
    26            last_name = person[1]
    27            sex = person[2]
    28            year_of_birth = person[3]
    29
    30            reality = src.person.factory(
    31                first_name=first_name,
    32                last_name=last_name,
    33                sex=sex,
    34                year_of_birth=year_of_birth,
    35            )
    36            my_expectation = (
    37                f'{first_name}, {last_name},'
    38                f' {sex}, {year_of_birth}'
    39            )
    40            self.assertEqual(reality, my_expectation)
    41
    42    def test_say_hello_function(self):
    
  • I use the class attribute for people in the for loop in test_say_hello_function

    42    def test_say_hello_function(self):
    43        # people = (
    44        #     ('jane', 'doe', 'F', 1991),
    45        #     ('joe', 'blow', 'M', 1996),
    46        #     ('mary', 'public', 'F', 2000),
    47        #     ('john', 'smith', 'M', 1980),
    48        #     ('first_name', 'last_name', 'F', 'a string'),
    49        # )
    50        # for person in people:
    51        for person in self.people:
    52            first_name = person[0]
    

    still green.

  • I remove the commented lines from test_say_hello_function

    42    def test_say_hello_function(self):
    43        for person in self.people:
    44            first_name = person[0]
    45            last_name = person[1]
    46            year_of_birth = person[3]
    47
    48            with self.subTest(
    49                first_name=first_name,
    50                year_of_birth=year_of_birth,
    51            ):
    52                try:
    53                    reality = src.person.say_hello(
    54                        first_name=first_name,
    55                        last_name=last_name,
    56                        year_of_birth=year_of_birth,
    57                    )
    58                except TypeError as error:
    59                    self.assertEqual(
    60                        error.args[0],
    61                        f"'{year_of_birth}' is not an integer"
    62                    )
    63                else:
    64                    my_expectation = (
    65                        f'Hello, my name is {first_name}'
    66                        f' {last_name} and I am'
    67                        f' {self.calculate_age(year_of_birth)}.'
    68                    )
    69                    self.assertEqual(reality, my_expectation)
    70
    71    def test_say_hello_method(self):
    
  • I use the class attribute for people in the for loop in test_say_hello_method

    71    def test_say_hello_method(self):
    72        # people = (
    73        #     ('jane', 'doe', 'F', 1991),
    74        #     ('joe', 'blow', 'M', 1996),
    75        #     ('mary', 'public', 'F', 2000),
    76        #     ('john', 'smith', 'M', 1980),
    77        #     ('first_name', 'last_name', 'F', 'a string'),
    78        # )
    79        # for person in people:
    80        for person in self.people:
    81            first_name = person[0]
    

    green.

  • I remove the commented lines from test_say_hello_method

     71    def test_say_hello_method(self):
     72        for person in self.people:
     73            first_name = person[0]
     74            last_name = person[1]
     75            sex = person[2]
     76            year_of_birth = person[3]
     77
     78            with self.subTest(
     79                first_name=first_name,
     80                year_of_birth=year_of_birth,
     81            ):
     82                try:
     83                    a_person = src.person.Person(
     84                        first_name=first_name,
     85                        last_name=last_name,
     86                        sex=sex,
     87                        year_of_birth=year_of_birth,
     88                    )
     89                except TypeError as error:
     90                    self.assertEqual(
     91                        error.args[0],
     92                        f"'{year_of_birth}' is not an integer"
     93                    )
     94                else:
     95                    self.assertEqual(
     96                        a_person.say_hello(),
     97                        (
     98                            f'Hello, my name is {first_name}'
     99                            f' {last_name} and I am'
    100                            f' {self.calculate_age(year_of_birth)}.'
    101                        )
    102                    )
    103
    104    def test_joe(self):
    
  • I add a git commit message

    git commit -am 'extract people class attribute'
    

extract test_can_person_vote

The tests in test_joe, test_jane, test_joe, test_mary and test_underage_citizen make an instance of the Person class, then call its can_vote method comparing the results in assertions.

The can_vote method of the Person class sends two parameters (age and response) when it calls the check_age method to return False or True for if a person cannot vote or can vote.

The inputs are

  • is the person younger than 18?

  • is the person a citizen?

The truth table for the can_vote is

age < 18

is citizen

can vote

True

True

False

True

False

False

False

True

True

False

False

False


RED: make it fail


I add a test with an assertion for if a person is younger than 18 AND is a citizen

age < 18

is citizen

can vote

True

True

False

 94                else:
 95                    self.assertEqual(
 96                        a_person.say_hello(),
 97                        (
 98                            f'Hello, my name is {first_name}'
 99                            f' {last_name} and I am'
100                            f' {self.calculate_age(year_of_birth)}.'
101                        )
102                    )
103
104    def test_can_person_vote(self):
105        truth_table = (
106            (datetime.date.today().year-17, True),
107        )
108        for row in truth_table:
109            with self.subTest(row=row):
110                a_person = src.person.Person(
111                    first_name='first_name',
112                    last_name='last_name',
113                    sex='F',
114                    year_of_birth=row[0],
115                    is_citizen=row[1],
116                )
117                self.assertEqual(a_person.can_vote(), True)
118
119    def test_joe(self):

the terminal is my friend, and shows AssertionError

SUBFAILED(row=(2009, True)) ...test_can_person_vote -
    AssertionError: False != True

GREEN: make it pass


I change True to False in the assertion for if a person is younger than 18 AND is a citizen

104    def test_can_person_vote(self):
105        truth_table = (
106            (datetime.date.today().year-17, True),
107        )
108        for row in truth_table:
109            with self.subTest(row=row):
110                a_person = src.person.Person(
111                    first_name='first_name',
112                    last_name='last_name',
113                    sex='F',
114                    year_of_birth=row[0],
115                    is_citizen=row[1],
116                )
117                self.assertEqual(a_person.can_vote(), False)
118
119    def test_joe(self):

the test passes.


REFACTOR: make it better


  • I add a tuple to the truth_table tuple for if a person is younger than 18 AND is NOT a citizen

    age < 18

    is citizen

    can vote

    True

    False

    False

    104    def test_can_person_vote(self):
    105        truth_table = (
    106            (datetime.date.today().year-17, True),
    107            (datetime.date.today().year-17, False),
    108        )
    109        for row in truth_table:
    

    the test is still green.

  • I add a tuple to the truth_table tuple for if a person is NOT younger than 18 AND is a citizen

    age < 18

    is citizen

    can vote

    False

    True

    True

    104    def test_can_person_vote(self):
    105        truth_table = (
    106            (datetime.date.today().year-17, True),
    107            (datetime.date.today().year-17, False),
    108            (datetime.date.today().year-18, True),
    109        )
    110        for row in truth_table:
    

    the terminal is my friend, and shows AssertionError

    SUBFAILED(row=(2008, True)) ...test_can_person_vote -
        AssertionError: True != False
    
  • I add the expectation to the tuples for the rows in the truth_table tuple

    104    def test_can_person_vote(self):
    105        truth_table = (
    106            (datetime.date.today().year-17, True, False),
    107            (datetime.date.today().year-17, False, False),
    108            (datetime.date.today().year-18, True, True),
    109        )
    110        for row in truth_table:
    
  • I use the index of the new values as the expectation of the assertion

    110        for row in truth_table:
    111            with self.subTest(row=row):
    112                a_person = src.person.Person(
    113                    first_name='first_name',
    114                    last_name='last_name',
    115                    sex='F',
    116                    year_of_birth=row[0],
    117                    is_citizen=row[1],
    118                )
    119                self.assertEqual(
    120                    a_person.can_vote(), row[3]
    121                )
    122
    123def test_joe(self):
    

    the terminal is my friend, and shows IndexError

    SUBFAILED(row=(2009, True, False)) ...test_can_person_vote -
        IndexError: tuple index out of range
    SUBFAILED(row=(2009, False, False)) ...test_can_person_vote -
        IndexError: tuple index out of range
    SUBFAILED(row=(2008, True, True)) ...test_can_person_vote -
        IndexError: tuple index out of range
    
  • I change the index to the right number

    119                self.assertEqual(
    120                    a_person.can_vote(), row[2]
    121                )
    

    the test passes.

  • I add a tuple to the truth_table tuple for if a person is NOT younger than 18 AND is NOT a citizen

    age < 18

    is citizen

    can vote

    False

    False

    False

    104    def test_can_person_vote(self):
    105        truth_table = (
    106            (datetime.date.today().year-17, True, False),
    107            (datetime.date.today().year-17, False, False),
    108            (datetime.date.today().year-18, True, True),
    109            (datetime.date.today().year-18, False, False),
    110        )
    111        for row in truth_table:
    112            with self.subTest(row=row):
    113                a_person = src.person.Person(
    114                    first_name='first_name',
    115                    last_name='last_name',
    116                    sex='F',
    117                    year_of_birth=row[0],
    118                    is_citizen=row[1],
    119                )
    120                self.assertEqual(
    121                    a_person.can_vote(), row[2]
    122                )
    123
    124    def test_joe(self):
    

    still green.

  • I remove the assertion for the can_vote method from test_joe

    124    def test_joe(self):
    125        first_name = 'joe'
    126        last_name = 'blow'
    127        sex = 'M'
    128        year_of_birth = 1996
    129
    130        joe = src.person.Person(
    131            first_name=first_name,
    132            last_name=last_name,
    133            sex=sex,
    134            year_of_birth=year_of_birth,
    135        )
    136        self.assertEqual(joe.can_get_license(), False)
    137
    138    def test_jane(self):
    
  • I remove the assertion for the can_vote method from test_jane

    138    def test_jane(self):
    139        first_name = 'jane'
    140        last_name = 'doe'
    141        sex = 'F'
    142        year_of_birth = 1991
    143
    144        jane = src.person.Person(
    145            first_name=first_name,
    146            last_name=last_name,
    147            sex=sex,
    148            year_of_birth=year_of_birth,
    149            passed_test=True,
    150        )
    151        self.assertEqual(jane.can_get_license(), True)
    152
    153    def test_john(self):
    
  • I remove the assertion for the can_vote method from test_john

    153    def test_john(self):
    154        first_name = 'john'
    155        last_name = 'smith'
    156        sex = 'M'
    157        year_of_birth = 1980
    158
    159        john = src.person.Person(
    160            first_name=first_name,
    161            last_name=last_name,
    162            sex=sex,
    163            year_of_birth=year_of_birth,
    164            is_citizen=False,
    165        )
    166        self.assertEqual(john.can_get_license(), False)
    167
    168    def test_mary(self):
    
  • I remove the assertion for the can_vote method from test_mary

    168    def test_mary(self):
    169        first_name = 'mary'
    170        last_name = 'public'
    171        sex = 'F'
    172        year_of_birth = 2000
    173
    174        mary = src.person.Person(
    175            first_name=first_name,
    176            last_name=last_name,
    177            sex=sex,
    178            year_of_birth=year_of_birth,
    179            is_citizen=False,
    180            passed_test=True,
    181        )
    182        self.assertEqual(mary.can_get_license(), True)
    183
    184    def test_underage_citizen(self):
    
  • I remove the assertion for the can_vote method from test_underage_citizen

    184    def test_underage_citizen(self):
    185        person = src.person.Person(
    186            first_name='first_name',
    187            last_name='last_name',
    188            sex='M',
    189            year_of_birth=datetime.date.today().year-17,
    190            is_citizen=True,
    191            passed_test=True,
    192        )
    193        self.assertEqual(
    194            person.can_get_license(), False
    195        )
    196
    197    def test_when_person_is_too_old_to_be_alive(self):
    
  • I add a git commit message

    git commit -am 'extract test_can_person_vote'
    

extract test_can_person_get_license

The tests in test_joe, test_jane, test_joe, test_mary and test_underage_citizen make an instance of the Person class, then call its can_get_license method comparing the results in assertions.

The can_get_license method of the Person class also sends two parameters when it calls the check_age method to return False or True for if a person cannot get a license or can get a license.

The inputs are

  • is the person younger than 18?

  • did the person pass the test?

The truth table for the can_get_license is

age < 18

passed test

can get license

True

True

False

True

False

False

False

True

True

False

False

False


RED: make it fail


I add a test with an assertion for if a person is NOT younger than 18 AND has NOT passed the test.

age < 18

passed test

can get license

False

False

False

120                self.assertEqual(
121                    a_person.can_vote(), row[2]
122                )
123
124    def test_can_person_get_license(self):
125        truth_table = (
126            (datetime.date.today().year-18, False, False),
127        )
128        for row in truth_table:
129            with self.subTest(row=row):
130                a_person = src.person.Person(
131                    first_name='first_name',
132                    last_name='last_name',
133                    sex='F',
134                    year_of_birth=row[0],
135                    passed_test=row[1],
136                )
137                self.assertEqual(
138                    a_person.can_get_license(), True
139                )
140
141    def test_joe(self):

the terminal is my friend, and shows AssertionError

SUBFAILED(row=(2008, False, False)) ...test_can_person_get_license
    - AssertionError: False != True

GREEN: make it pass


I change the expectation of the assertion

137                self.assertEqual(
138                    a_person.can_get_license(), False
139                )

the test passes.


REFACTOR: make it better


  • I add a tuple to the truth_table tuple for if a person is NOT younger than 18 AND has passed the test

    age < 18

    passed test

    can get license

    False

    True

    True

    124    def test_can_person_get_license(self):
    125        truth_table = (
    126            (datetime.date.today().year-18, True, True),
    127            (datetime.date.today().year-18, False, False),
    128        )
    129        for row in truth_table:
    

    the terminal is my friend, and shows AssertionError

    SUBFAILED(row=(2008, True, True)) ...
        test_can_person_get_license - AssertionError:
            True != False
    
  • I use the index to change the expectation to the last item of the row tuple

    129        for row in truth_table:
    130            with self.subTest(row=row):
    131                a_person = src.person.Person(
    132                    first_name='first_name',
    133                    last_name='last_name',
    134                    sex='F',
    135                    year_of_birth=row[0],
    136                    is_citizen=row[1],
    137                )
    138                self.assertEqual(
    139                    a_person.can_get_license(), row[-1]
    140                )
    141
    142    def test_joe(self):
    

    the test passes.

  • I add a tuple to the truth_table tuple for if a person is younger than 18 AND has NOT passed the test

    age < 18

    passed test

    can get license

    True

    False

    False

    124    def test_can_person_get_license(self):
    125        truth_table = (
    126            (datetime.date.today().year-17, False, False),
    127            (datetime.date.today().year-18, True, True),
    128            (datetime.date.today().year-18, False, False),
    129        )
    130        for row in truth_table:
    

    the test is still green.

  • I add a tuple to the truth_table tuple for if a person is younger than 18 AND has passed the test

    age < 18

    passed test

    can get license

    True

    True

    False

    124    def test_can_person_get_license(self):
    125        truth_table = (
    126            (datetime.date.today().year-17, True, False),
    127            (datetime.date.today().year-17, False, False),
    128            (datetime.date.today().year-18, True, True),
    129            (datetime.date.today().year-18, False, False),
    130        )
    131        for row in truth_table:
    

    still green.

  • I remove test_joe, test_jane, test_john, test_mary and test_underage_citizen since they are now repetitions of test_can_person_get_license

    124    def test_can_person_get_license(self):
    125        truth_table = (
    126            (datetime.date.today().year-17, True, False),
    127            (datetime.date.today().year-17, False, False),
    128            (datetime.date.today().year-18, True, True),
    129            (datetime.date.today().year-18, False, False),
    130        )
    131        for row in truth_table:
    132            with self.subTest(row=row):
    133                a_person = src.person.Person(
    134                    first_name='first_name',
    135                    last_name='last_name',
    136                    sex='F',
    137                    year_of_birth=row[0],
    138                    is_citizen=row[1],
    139                )
    140                self.assertEqual(
    141                    a_person.can_get_license(), row[-1]
    142                )
    143
    144    def test_when_person_is_too_old_to_be_alive(self):
    
  • I add a git commit message

    git commit -am \
    'extract test_can_person_get_license'
    

extract truth_table class attribute

The truth_table tuple is used in both test_can_person_vote and test_can_person_get_license .

  • I add a class attribute for the truth_table tuple to the TestPerson class

     6class TestPerson(unittest.TestCase):
     7
     8    people = (
     9        ('jane', 'doe', 'F', 1991),
    10        ('joe', 'blow', 'M', 1996),
    11        ('mary', 'public', 'F', 2000),
    12        ('john', 'smith', 'M', 1980),
    13        ('first_name', 'last_name', 'F', 'a string'),
    14    )
    15    truth_table = (
    16        (datetime.date.today().year-17, True, False),
    17        (datetime.date.today().year-17, False, False),
    18        (datetime.date.today().year-18, True, True),
    19        (datetime.date.today().year-18, False, False),
    20    )
    21
    22    @staticmethod
    23    def calculate_age(year_of_birth):
    
  • I use the class attribute for truth_table in the for loop in test_can_person_vote

    the test is still green.

  • I remove the commented lines from test_can_person_vote

    110
    
  • I use the class attribute for truth_table in the for loop in test_can_person_vote

    still green.

  • I remove the commented lines from test_can_person_vote

    110
    

use assertRaises with test_when_person_is_too_old_to_be_alive


test_when_year_of_birth_is_not_an_integer


close the project

  • I close test_person.py and src/person/__init__.py

  • I use q on the keyboard to leave the tests. The terminal goes back to the command line.

  • I change directory to the parent of person

    cd ..
    

review

My tests still have problems:


code from the chapter

Do you want to see all the CODE I typed in this chapter?


what is next?

Would you like to test handling Exceptions in tests?


rate pumping python

If this has been a 7 star experience for you, please CLICK HERE to leave a 5 star review of pumping python. It helps other people get into the book too.