how to make a person with generators


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    this_year = datetime.date.today().year
 9    younger_than_18 = this_year - 17
10    not_younger_than_18 = this_year - 18
11
12    first_name = 'first_name'
13    last_name = 'last_name'
14    sex = 'M'
person/tests/test_person.py
16    people = (
17        (
18            # is_citizen, passed_test
19            not_younger_than_18, True, True,
20            True, True,
21        ),
22        (
23            # is_citizen, not passed_test
24            not_younger_than_18, True, False,
25            True, False,
26        ),
27        (
28            # not is_citizen, passed_test
29            not_younger_than_18, False, True,
30            False, True,
31        ),
person/tests/test_person.py
32        (
33            # not is_citizen, not passed_test
34            not_younger_than_18, False, False,
35            False, False,
36        ),
37        (
38            # underage, is_citizen, passed_test
39            younger_than_18, True, True,
40            False, False,
41        ),
42        (
43            # underage, not is_citizen, passed_test
44            younger_than_18, False, True,
45            False, False,
46        ),
person/tests/test_person.py
47        (
48            # underage, is_citizen, not passed_test
49            younger_than_18, True, False,
50            False, False,
51        ),
52        (
53            # underage, not is_citizen, not passed_test
54            younger_than_18, False, False,
55            False, False,
56        ),
57        # year_of_birth is not an integer
58        (None, False, False,),
59        (2026.0, False, False,),
60        ('2026', False, False,),
61        ((2026,), False, False,),
62    )
63
64    def calculate_age(self, year_of_birth):
65        return self.this_year - year_of_birth
person/tests/test_person.py
67    def test_person_object(self):
68        for a_person in self.people:
69            year_of_birth = a_person[0]
70
71            with self.subTest(a_person):
72                try:
73                    an_instance = src.person.Person(
74                        first_name=self.first_name,
75                        last_name=self.last_name,
76                        sex=self.sex,
77                        year_of_birth=year_of_birth,
78                        is_citizen=a_person[1],
79                        passed_test=a_person[2],
80                    )
81                except TypeError as error:
82                    self.assertEqual(
83                        error.args[0],
84                        f"'{year_of_birth}' is not an integer"
85                    )
86                else:
87                    self.assertEqual(
88                        an_instance.say_hello(),
89                        (
90                            f'Hello, my name is {self.first_name}'
91                            f' {self.last_name} and I am'
92                            f' {self.calculate_age(year_of_birth)}.'
93                        )
94                    )
95                    self.assertEqual(an_instance.can_vote(), a_person[3])
96                    self.assertEqual(
97                        an_instance.can_get_license(), a_person[4]
98                    )
99
person/tests/test_person.py
100    def test_say_hello_function(self):
101        for a_person in self.people:
102            year_of_birth = a_person[0]
103
104            with self.subTest(a_person):
105                try:
106                    reality = src.person.say_hello(
107                        first_name=self.first_name,
108                        last_name=self.last_name,
109                        year_of_birth=year_of_birth,
110                    )
111                except TypeError as error:
112                    self.assertEqual(
113                        error.args[0],
114                        f"'{year_of_birth}' is not an integer"
115                    )
116                else:
117                    self.assertEqual(
118                        reality,
119                        (
120                            f'Hello, my name is {self.first_name}'
121                            f' {self.last_name} and I am'
122                            f' {self.calculate_age(year_of_birth)}.'
123                        )
124                    )
person/tests/test_person.py
126    def test_person_factory(self):
127        for a_person in self.people:
128            year_of_birth = a_person[0]
129
130            with self.subTest(a_person):
131                self.assertEqual(
132                    src.person.person(
133                        first_name=self.first_name,
134                        last_name=self.last_name,
135                        sex=self.sex,
136                        year_of_birth=year_of_birth,
137                    ),
138                    (
139                        f'{self.first_name}, {self.last_name},'
140                        f' {self.sex}, {year_of_birth}'
141                    )
142                )
143
person/tests/test_person.py
144    def test_if_person_is_not_alive(self):
145        for year_of_birth in (
146            self.this_year+1,
147            self.this_year-121,
148        ):
149            age = self.calculate_age(year_of_birth)
150
151            with (
152                self.subTest(year_of_birth),
153                self.assertRaisesRegex(
154                    ValueError,
155                    f'a {age} year old person is not alive.'
156                ),
157            ):
158                src.person.Person(
159                    first_name=self.first_name,
160                    last_name=self.last_name,
161                    sex=self.sex,
162                    year_of_birth=year_of_birth,
163                )
person/tests/test_person.py
165    def test_dir_person_object(self):
166        self.assertEqual(
167            dir(src.person.Person),
168            [
169                '__class__', '__delattr__', '__dict__',
170                '__dir__', '__doc__', '__eq__',
171                '__firstlineno__', '__format__', '__ge__',
172                '__getattribute__', '__getstate__', '__gt__',
173                '__hash__', '__init__', '__init_subclass__',
174                '__le__', '__lt__', '__module__', '__ne__',
175                '__new__', '__reduce__', '__reduce_ex__',
176                '__repr__', '__setattr__', '__sizeof__',
177                '__static_attributes__', '__str__',
178                '__subclasshook__', '__weakref__',
179                'can_get_license', 'can_vote', 'check_age',
180                'say_hello',
181            ]
182        )
person/tests/test_person.py
184    def test_dir_person_instance(self):
185        self.assertEqual(
186            dir(
187                src.person.Person(
188                    first_name='first_name',
189                    last_name='last_name',
190                    sex='M',
191                    year_of_birth=2026,
192                )
193            ),
194            [
195                '__class__', '__delattr__', '__dict__',
196                '__dir__', '__doc__', '__eq__',
197                '__firstlineno__', '__format__', '__ge__',
198                '__getattribute__', '__getstate__', '__gt__',
199                '__hash__', '__init__', '__init_subclass__',
200                '__le__', '__lt__', '__module__', '__ne__',
201                '__new__', '__reduce__', '__reduce_ex__',
202                '__repr__', '__setattr__', '__sizeof__',
203                '__static_attributes__', '__str__',
204                '__subclasshook__', '__weakref__',
205                'age', 'can_get_license', 'can_vote',
206                'check_age', 'first_name', 'is_citizen',
207                'last_name', 'passed_test', 'say_hello',
208                'sex', 'year_of_birth',
209            ]
210        )
person/tests/test_person.py
213# Exceptions seen
214# AssertionError
215# NameError
216# TypeError
217# AttributeError
218# SyntaxError

open the project

  • I open a terminal

  • I change directory to the project

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

  • I use pytest-watcher to run the tests automatically

    terminal
    uv run pytest-watcher . --now --patterns "**/*.py"
    

extract test_person_factory

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

self.assertEqual(
    src.person.person(
        first_name=first_name,
        last_name=last_name,
        sex=sex,
        year_of_birth=year_of_birth,
    ),
    (
        f'{first_name}, {last_name},'
        f' {sex}, {year_of_birth}'
    )
)

I can make one function with a for loop to remove the repetition.


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

    the test is still green.

  • 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 a_person in (
        ('joe', 'blow', 'M', 1996),
    ):
    
  • I use the index of each item in joe for the parameters when the test calls the person function

    for a_person in people:
    ├── a_person = ('joe', 'blow', 'M', 1996)
    └── self.assert_person_factory_works(
            first_name=a_person[0],     # 'joe'
            last_name=a_person[1],      # 'blow'
            sex=a_person[2],            # 'M'
            year_of_birth=a_person[3],  # 1996
        )
        └── def assert_person_factory_works(
                self, first_name, last_name,
                sex, year_of_birth
            ):
            ├── first_name    = 'joe'   # a_person[0]
            ├── last_name     = 'blow'  # a_person[1]
            ├── sex           = 'M'     # a_person[2]
            ├── year_of_birth = 1996    # a_person[3]
            └── self.assertEqual(
            ┌────── src.person.person(
            │           first_name=first_name,        # 'joe'
            │           last_name=last_name,          # 'blow'
            │           sex=sex,                      # 'M'
            │           year_of_birth=year_of_birth,  # 1996
            │       ),
            │       (
            │           f'{first_name}, {last_name},'
            │           f' {sex}, {year_of_birth}'
            │       )
            │   )
            └────── src/person/__init__.py
                    └── def person(
                            first_name, last_name,
                            sex, year_of_birth,
                        ):
                        ├── first_name    = 'joe'   # a_person[0]
                        ├── last_name     = 'blow'  # a_person[1]
                        ├── sex           = 'M'     # a_person[2]
                        ├── year_of_birth = 1996    # a_person[3]
                        └── return (
                                f'{first_name}, {last_name},'
                                f' {sex}, {year_of_birth}'
                            )
                            return 'joe, blow, M, 1996'
    
  • I add jane to the tuple of people

    still green, because the result when the person function is called with 'jane', 'doe', 'F' and '1991' is 'jane, doe, F, 1991'.

        for a_person in (
    ┌───┴── ('jane', 'doe', 'F', 1991),
    │       ('joe', 'blow', 'M', 1996),
    │   ):
    ├────── a_person = ('jane', 'doe', 'F', 1991)
    └────── self.assert_person_factory_works(
                first_name=a_person[0],     # 'jane'
                last_name=a_person[1],      # 'doe'
                sex=a_person[2],            # 'F'
                year_of_birth=a_person[3],  # 1991
            )
            └── def assert_person_factory_works(
                    self, first_name, last_name,
                    sex, year_of_birth
                ):
                └── self.assertEqual(
                ┌────── src.person.person(
                │           first_name=first_name,        # 'jane'
                │           last_name=last_name,          # 'doe'
                │           sex=sex,                      # 'F'
                │           year_of_birth=year_of_birth,  # 1991
                │       ),
                │       (
                │           f'{first_name}, {last_name},'
                │           f' {sex}, {year_of_birth}'
                │       )
                │   )
                └────── src/person/__init__.py
                        └── def person(
                                first_name, last_name,
                                sex, year_of_birth,
                            ):
                            ├── first_name    = 'jane'  # a_person[0]
                            ├── last_name     = 'doe'   # a_person[1]
                            ├── sex           = 'F'     # a_person[2]
                            ├── year_of_birth = 1991    # a_person[3]
                            └── return (
                                    f'{first_name}, {last_name},'
                                    f' {sex}, {year_of_birth}'
                                )
                                return 'jane, doe, M, 1991'
    
  • I add a tuple for mary

    still green.

        for a_person in (
        │   ('joe', 'blow', 'M', 1996),
        │   ('jane', 'doe', 'F', 1991),
    ┌───┴── ('mary', 'public', 'F', 2000),
    │   ):
    ├────── a_person = ('mary', 'public', 'F', 2000)
    └────── self.assert_person_factory_works(
                first_name=a_person[0],     # 'mary'
                last_name=a_person[1],      # 'public'
                sex=a_person[2],            # 'F'
                year_of_birth=a_person[3],  # 2000
            )
            └── def assert_person_factory_works(
                    self, first_name, last_name,
                    sex, year_of_birth
                ):
                └── self.assertEqual(
                ┌────── src.person.person(
                │           first_name=first_name,        # 'mary'
                │           last_name=last_name,          # 'public'
                │           sex=sex,                      # 'F'
                │           year_of_birth=year_of_birth,  # 2000
                │       ),
                │       (
                │           f'{first_name}, {last_name},'
                │           f' {sex}, {year_of_birth}'
                │       )
                │   )
                └────── src/person/__init__.py
                        └── def person(
                                first_name, last_name,
                                sex, year_of_birth,
                            ):
                            ├── first_name    = 'mary'    # a_person[0]
                            ├── last_name     = 'public'  # a_person[1]
                            ├── sex           = 'F'       # a_person[2]
                            ├── year_of_birth = 2000      # a_person[3]
                            └── return (
                                    f'{first_name}, {last_name},'
                                    f' {sex}, {year_of_birth}'
                                )
                                return 'mary, public, M, 2000'
    
  • I add a tuple for john

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

        for a_person in (
        │   ('joe', 'blow', 'M', 1996),
        │   ('jane', 'doe', 'F', 1991),
        │   ('mary', 'public', 'F', 2000),
    ┌───┴── ('john', 'smith', 'M', 1980),
    │   ):
    ├────── a_person = ('john', 'smith', 'M', 1980)
    └────── self.assert_person_factory_works(
                first_name=a_person[0],     # 'john'
                last_name=a_person[1],      # 'smith'
                sex=a_person[2],            # 'M'
                year_of_birth=a_person[3],  # 1980
            )
            └── def assert_person_factory_works(
                    self, first_name, last_name,
                    sex, year_of_birth
                ):
                └── self.assertEqual(
                ┌────── src.person.person(
                │           first_name=first_name,        # 'john'
                │           last_name=last_name,          # 'smith'
                │           sex=sex,                      # 'M'
                │           year_of_birth=year_of_birth,  # 1980
                │       ),
                │       (
                │           f'{first_name}, {last_name},'
                │           f' {sex}, {year_of_birth}'
                │       )
                │   )
                └────── src/person/__init__.py
                        └── def person(
                                first_name, last_name,
                                sex, year_of_birth,
                            ):
                            ├── first_name    = 'john'    # a_person[0]
                            ├── last_name     = 'smith'  # a_person[1]
                            ├── sex           = 'M'       # a_person[2]
                            ├── year_of_birth = 1980      # a_person[3]
                            └── return (
                                    f'{first_name}, {last_name},'
                                    f' {sex}, {year_of_birth}'
                                )
                                return 'john, smith, M, 1980'
    
  • I add a tuple for a person with a string as the year_of_birth

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

  • I remove the call to the assert_person_factory_works method from test_joe since it is now a repetition

  • I remove the call to the assert_person_factory_works method from test_jane since it is now a repetition

  • I remove the call to the assert_person_factory_works method from test_john since it is now a repetition

  • I remove the call to the assert_person_factory_works method from test_mary since it is now a repetition

  • I no longer need the assert_person_factory_works method because it is only used by test_person_factory

    the terminal is my friend, and shows AssertionError

    AssertionError: 'jane, doe, F, 1991' != None
    
  • I change the expectation of the assertion to 'jane, doe, F, 1991'

    the terminal shows AssertionError

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

    because the for loop moved to the next item in the people tuple ('joe')

  • I change the expectation to the f-string from the assert_person_factory_works method

    the test passes.

  • I remove the call to the assert_person_factory_works method from test_person_factory

    the tests are still green.

  • I remove the assert_person_factory_works method from the TestPerson object

  • I add a git commit message

    terminal
    git commit -am 'extract test_person_factory'
    

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.

self.assertEqual(
    src.person.say_hello(
        first_name=first_name,
        last_name=last_name,
        year_of_birth=year_of_birth,
    ),
    (
        f'Hello, my name is {first_name}'
        f' {last_name} and I am'
        f' {self.calculate_age(year_of_birth)}.'
    )
)

I can make one function with a for loop to remove the repetition.


RED: make it fail


I add a test for the say_hello function

30      def assert_person_can_say_hello(
31              self,first_name, last_name,
32              sex, year_of_birth,
33          ):
34          self.assertEqual(
35              src.person.Person(
36                  first_name=first_name,
37                  last_name=last_name,
38                  sex=sex,
39                  year_of_birth=year_of_birth,
40              ).say_hello(),
41              (
42                  f'Hello, my name is {first_name}'
43                  f' {last_name} and I am'
44                  f' {self.calculate_age(year_of_birth)}.'
45              )
46          )
48      def test_say_hello_function(self):
49          people = (
50              ('jane', 'doe', 'F', 1991),
51              ('joe', 'blow', 'M', 1996),
52              ('mary', 'public', 'F', 2000),
53              ('john', 'smith', 'M', 1980),
54              ('first_name', 'last_name', 'F', 'a_string')
55          )
56          for a_person in people:
57              first_name = a_person[0]
58              last_name = a_person[1]
59              year_of_birth = a_person[3]
60
61              self.assert_say_hello_works(
62                  first_name=first_name,
63                  last_name=last_name,
64                  year_of_birth=year_of_birth,
65              )
66
67      def test_person_factory(self):

the terminal is my friend, and shows TypeError

E           TypeError

because

  • the the say_hello function calls the calculate_age function

  • the calculate_age function raises TypeError when year_of_birth is not an integer. year_of_birth is a string in the last tuple in the people tuple (('first_name', 'last_name', 'F', 'a_string'))

        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 = a_person[0]    # 'first_name'
    ├────── last_name = a_person[1]     # 'last_name'
    ├────── year_of_birth = a_person[3] # 'a string'
    └────── self.assert_say_hello_works(
                first_name=first_name,
                last_name=last_name,
                year_of_birth=year_of_birth,
            )
            └── def assert_person_factory_works(
                    self, first_name, last_name,
                    sex, year_of_birth
                ):
                └── self.assertEqual(
                ┌────── src.person.say_hello(
                │           first_name=first_name,
                │           last_name=last_name,
                │           year_of_birth=year_of_birth,
                │       ),
                │       (
                │           f'Hello, my name is {first_name}'
                │           f' {last_name} and I am'
                │           f' {self.calculate_age(year_of_birth)}.'
                │       )
                │   )
                └────── src/person/__init__.py
                        └── def say_hello(
                                    first_name, last_name, year_of_birth
                                ):
                                ├── first_name    = 'first_name'  # a_person[0]
                                ├── last_name     = 'last_name'   # a_person[1]
                                ├── year_of_birth = 'a string'    # a_person[3]
                                └── 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
                                                ...
    

GREEN: make it pass



REFACTOR: make it better


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

    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 object in the except block

    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 add_note is

    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

    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 args is

    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

    the test passes.

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

    the terminal is my friend, and shows AssertionError

    AssertionError: 'BOOM!!!' != "'a string' is not an integer"
    

    because the error message is not correct

        for a_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'),
    │   ):
    ├────── first_name = a_person[0]    # 'first_name'
    ├────── last_name = a_person[1]     # 'last_name'
    ├────── year_of_birth = a_person[3] # 'a string'
    └────── self.assert_say_hello_works(
                first_name=first_name,
                last_name=last_name,
                year_of_birth=year_of_birth,
            )
            └── def assert_say_hello_works(
                    self, first_name, last_name,
                    year_of_birth,
                ):
                └── 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,
                                  ):
                                  └── 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:
                          ...
    
  • I undo the change

    the test is green again.

  • I remove the call to the assert_say_hello_works method from test_joe since it is now a repetition

  • I remove the call to the assert_say_hello_works method from test_jane since it is now a repetition

  • I remove the call to the assert_say_hello_works method from test_john since it is now a repetition

  • I remove the call to the assert_say_hello_works method from test_mary since it is now a repetition

  • I no longer need the assert_say_hello_works method because it is only used by test_say_hello_function

    the terminal is my friend, and shows AssertionError

    AssertionError: 'Hello, my name is jane doe and I am YY.' != None
    
  • I change the expectation to 'Hello, my name is jane doe and I am YY.'

    the terminal shows AssertionError

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

    because the for loop moved to the next item in the people tuple ('joe')

  • I change the expectation to the f-string from the assert_say_hello_works method

    the test passes.

  • I remove the call to the assert_say_hello_works method from test_say_hello_function

  • I remove the assert_say_hello_works method from the TestPerson object

  • I add a git commit message

    terminal
    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

    • If the error message is correct the test passes

          for a_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'),
      │   ):
      ├────── first_name = a_person[0]    # 'first_name'
      ├────── last_name = a_person[1]     # 'last_name'
      ├────── year_of_birth = a_person[3] # 'a string'
      └────── 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'  # a_person[0]
                            ├── last_name     = 'last_name'   # a_person[1]
                            ├── year_of_birth = 'a string'    # a_person[3]
                            └── 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 calculate_age function from 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'),
    │   ):
    ├────── first_name = a_person[0]    # 'john'
    ├────── last_name = a_person[1]     # 'smith'
    ├────── year_of_birth = a_person[3] # 1980
    └───┬── 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'  # a_person[0]
        │                 ├── last_name     = 'smith' # a_person[1]
        │                 ├── year_of_birth = 1980    # a_person[3]
        │                 └── 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 = 1980 # a_person[3]
        │                             ├── ...
        │                             └── return age
        │   except TypeError as error:
        │       ...
        └── else:
            └── self.assertEqual(
                    reality,
                    (
                        f'Hello, my name is {first_name}'
                        f' {last_name} and I am'
                        f' {self.calculate_age(year_of_birth)}.'
                    )   │
                )       └── def calculate_age(self, year_of_birth):
                            ├── year_of_birth = 1980  # a_person[3]
                            └── return self.this_year - year_of_birth
    

extract test_say_hello_method

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

self.assertEqual(
    src.person.Person(
        first_name=first_name,
        last_name=last_name,
        sex=sex,
        year_of_birth=year_of_birth,
    ).say_hello(),
    (
        f'Hello, my name is {first_name}'
        f' {last_name} and I am'
        f' {self.calculate_age(year_of_birth)}.'
    )
)

I can make one function with a for loop to remove the repetition.


RED: make it fail


I add a test for the say_hello method of the Person object

13      def assert_person_can_say_hello(
14              self,first_name, last_name,
15              sex, year_of_birth,
16          ):
17          self.assertEqual(
18              src.person.Person(
19                  first_name=first_name,
20                  last_name=last_name,
21                  sex=sex,
22                  year_of_birth=year_of_birth,
23              ).say_hello(),
24              (
25                  f'Hello, my name is {first_name}'
26                  f' {last_name} and I am'
27                  f' {self.calculate_age(year_of_birth)}.'
28              )
29          )
31      def test_say_hello_method(self):
32          people = (
33              ('jane', 'doe', 'F', 1991),
34              ('joe', 'blow', 'M', 1996),
35              ('mary', 'public', 'F', 2000),
36              ('john', 'smith', 'M', 1980),
37              ('first_name', 'last_name', 'F', 'a_string')
38          )
39          for a_person in people:
40              first_name = a_person[0]
41              last_name = a_person[1]
42              sex = a_person[2]
43              year_of_birth = a_person[3]
44
45              self.assert_person_can_say_hello(
46                  first_name=first_name,
47                  last_name=last_name,
48                  sex=sex,
49                  year_of_birth=year_of_birth
50              )
51
52      def test_say_hello_function(self):

the terminal is my friend, and shows TypeError

E           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 in test_say_hello_method

    the terminal is my friend, and shows TypeError

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

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

    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
    

    this is clearer.


GREEN: make it pass



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

    the terminal is my friend, and shows ASsertionError

    SUBFAILED(first_name='first_name', year_of_birth='a string') ...
        test_say_hello_method - AssertionError:
            'BAP!!!' != "'a string' is not an integer"
    FAILED tests/test_person.py...
        test_say_hello_function - AssertionError:
            'BAP!!!' != "'a string' is not an integer"
    

    because the error messages do not match

        for a_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'),
    │   ):
    ├────── first_name = a_person[0]    # 'first_name'
    ├────── last_name = a_person[1]     # 'last_name'
    ├────── sex = a_person[2]           # 'F'
    ├────── year_of_birth = a_person[3] # 'a string'
    └────── with self.subTest(
                first_name=first_name,
                year_of_birth=year_of_birth,
            ):
            └── self.assert_person_can_say_hello(
                    first_name=first_name,
                    last_name=last_name,
                    sex=sex,
                    year_of_birth=year_of_birth
                )
                └── def assert_person_can_say_hello(
                        self,first_name, last_name,
                        sex, year_of_birth,
                    ):
                    └── try:
                            reality = src.person.Person(
                                first_name=first_name,
                                last_name=last_name,
                                sex=sex,
                                year_of_birth=year_of_birth,
                            ).say_hello()
                            └── 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('BAP!!!')
                    └── except TypeError as error:
                        └── self.assertEqual(
                                error.args[0],
                                f"'{year_of_birth}' is not an integer"
                            )
                            └── raise AssertionError
                        else:
                            ...
    
  • 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

    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:
            'BAP!!!' != "'a string' is not an integer"
    SUBFAILED(first_name='first_name', year_of_birth='a string') ...
        test_say_hello_function - AssertionError:
            'BAP!!!' != "'a string' is not an integer"
    
  • I change the error message in the calculate_age function for if the value of the year_of_birth parameter is not an integer back to the correct message, in src/person/__init__.py

    the tests are green again.

  • I remove the call to the assert_person_can_say_hello from test_joe

  • I remove the call to the assert_person_can_say_hello from test_jane

  • I remove the call to the assert_person_can_say_hello from test_john

  • I remove the call to the assert_person_can_say_hello from test_mary

  • I no longer need the assert_person_can_say_hello method because it is only used by test_say_hello_method

    the terminal is my friend, and shows AssertionError for each one of the people with an integer for year_of_birth

    SUBFAILED(first_name='jane', year_of_birth=1991) ... -
        AssertionError: 'Hello, my name is jane doe and I am YY.' != None
    SUBFAILED(first_name='joe', year_of_birth=1996) ... -
        AssertionError: 'Hello, my name is joe blow and I am XX.' != None
    SUBFAILED(first_name='mary', year_of_birth=2000) ... -
        AssertionError: 'Hello, my name is mary public and I am ZZ.' != None
    SUBFAILED(first_name='john', year_of_birth=1980) ... -
        AssertionError: 'Hello, my name is john smith and I am AA.' != None
    
  • I change the expectation of the assertion to 'Hello, my name is jane doe and I am YY.'

    the terminal shows AssertionError

    SUBFAILED(first_name='joe', year_of_birth=1996) ...test_say_hello_method -
        AssertionError: 'Hello, my name is joe blow and I am XX.'
                     != 'Hello, my name is jane doe and I am YY.'
    SUBFAILED(first_name='mary', year_of_birth=2000) ...test_say_hello_method -
        AssertionError: 'Hello, my name is mary public and I am ZZ.'
                     != 'Hello, my name is jane doe and I am YY.'
    SUBFAILED(first_name='john', year_of_birth=1980) ...test_say_hello_method -
        AssertionError: 'Hello, my name is john smith and I am AA.'
                     != 'Hello, my name is jane doe and I am YY.'.
    

    the assertion now fails for the other people in the people tuple.

  • I change the expectation to the f-string from the assert_person_can_say_hello method

    the test passes.

  • I remove the call to the assert_person_factory_works method from test_say_hello_method

  • I remove the assert_person_can_say_hello method from the TestPerson object

  • I add a git commit message

    terminal
    git commit -am 'extract test_say_hello_method'
    

For each person in the people tuple, this test makes an instance of the Person object which makes an age attribute by calling the calculate_age function 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

    • If the error message is correct, the test passes

          for a_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'),
      │   ):
      ├────── first_name = a_person[0]    # 'first_name'
      ├────── last_name = a_person[1]     # 'last_name'
      ├────── sex = a_person[2]           # 'F'
      ├────── year_of_birth = a_person[3] # 'a string'
      └────── with self.subTest(
                  first_name=first_name,
                  year_of_birth=year_of_birth,
              ):
              └── try:
                      reality = src.person.Person(
                          first_name=first_name,
                          last_name=last_name,
                          sex=sex,
                          year_of_birth=year_of_birth,
                      ).say_hello()
                      └── 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 from the __init__ method of the Person object 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'),
    │   ):
    ├────── first_name = a_person[0]    # 'joe'
    ├────── last_name = a_person[1]     # 'blow'
    ├────── sex = a_person[2]           # 'M'
    ├────── year_of_birth = a_person[3] # 1996
    └────── with self.subTest(
                first_name=first_name,
                year_of_birth=year_of_birth,
            ):
            ├── try:
            │       reality = src.person.Person(
            │           first_name=first_name,
            │           last_name=last_name,
            │           sex=sex,
            │           year_of_birth=year_of_birth,
            │       ).say_hello()
            │       ├── 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
            │       │                        ):
            │       │                        └── def calculate_age(year_of_birth):
            │       │                            ├── year_of_birth = 1996
            │       │                            ├── ...
            │       │                            └── return age
            │       └── def say_hello(self):
            │           └── return (
            │                   f'Hello, my name is {self.first_name}'
            │                   f' {self.last_name} and I am {self.age}.'
            │               )
            ├── except TypeError as error:
            │       ...
            └── else:
                └── self.assertEqual(
                        reality,
                        (
                            f'Hello, my name is {first_name}'
                            f' {last_name} and I am'
                            f' {self.calculate_age(year_of_birth)}.'
                        )
                    )   │
                        └── def calculate_age(self, year_of_birth):
                            ├── year_of_birth = 1996
                            └── return self.this_year - year_of_birth
    

extract people object attribute

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


extract test_if_person_can_vote

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

an_instance = src.person.Person(
    first_name=first_name,
    last_name=last_name,
    sex=sex,
    year_of_birth=year_of_birth,
    is_citizen=False | True,
    passed_test=False | True,
)
self.assertEqual(an_instance.can_vote(), False | True)

I can make one function with a for loop to remove the repetition.

The can_vote method of the Person object 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 based on the following inputs

  • is the person younger than 18?

  • is the person a citizen?

The truth table for the can_vote method 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

 84      def test_person_factory(self):
 85          for a_person in self.people:
 86              first_name = a_person[0]
 87              last_name = a_person[1]
 88              sex = a_person[2]
 89              year_of_birth = a_person[3]
 90
 91              self.assertEqual(
 92                  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                  (
 99                      f'{first_name}, {last_name},'
100                      f' {sex}, {year_of_birth}'
101                  )
102              )
104      def test_if_person_can_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                  an_instance = 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(an_instance.can_vote(), True)
118
119      def test_joe(self):

the terminal is my friend, and shows AssertionError

SUBFAILED(row=(2009, True)) ...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_if_person_can_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                an_instance = 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(an_instance.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

    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

    the terminal is my friend, and shows AssertionError

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

  • I use the index of the new values as the expectation of the assertion

    the terminal is my friend, and shows IndexError

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

    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

    still green.

  • I remove the assertion for the can_vote method from test_joe

  • I remove the assertion for the can_vote method from test_jane

  • I remove the assertion for the can_vote method from test_john

  • I remove the assertion for the can_vote method from test_mary

  • I remove the assertion for the can_vote method from test_underage_citizen

  • I add a git commit message

    terminal
    git commit -am 'extract test_if_person_can_vote'
    

extract test_if_person_can_get_license

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

an_instance = src.person.Person(
    first_name=first_name,
    last_name=last_name,
    sex=sex,
    year_of_birth=year_of_birth,
    is_citizen=False | True,
    passed_test=False | True,
)
self.assertEqual(
    an_instance.can_get_license(), False | True
)

I can make one function with a for loop to remove the repetition.

The can_get_license method of the Person object 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 based on the following inputs

  • 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(an_instance.can_vote(), row[2])
121
122      def test_if_person_can_get_license(self):
123          truth_table = (
124              (datetime.date.today().year-18, False, False),
125          )
126          for row in truth_table:
127              with self.subTest(row=row):
128                  an_instance = src.person.Person(
129                      first_name='first_name',
130                      last_name='last_name',
131                      sex='M',
132                      year_of_birth=row[0],
133                      passed_test=row[1],
134                  )
135                  self.assertEqual(
136                      an_instance.can_get_license(), True
137                  )
138
139      def test_joe(self):

the terminal is my friend, and shows AssertionError

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

GREEN: make it pass


I change the expectation of the assertion

135                self.assertEqual(
136                    an_instance.can_get_license(), False
137                )

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

    the terminal is my friend, and shows AssertionError

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

    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

    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

    still green.

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

  • I add a git commit message

    terminal
    git commit -am \
    'extract test_if_person_can_get_license'
    

extract truth_table object attribute

The truth_table tuple is used in both test_if_person_can_vote and test_if_person_can_get_license. I can make an object attribute to remove the repetition.


extract test_person_object

test_say_hello_method, test_if_person_can_vote and test_if_person_can_get_license make an instance of the Person object, then call methods and compare the results in assertions

reality = src.person.Person(
    first_name=first_name,
    last_name=last_name,
    sex=sex,
    year_of_birth=year_of_birth,
).say_hello()

self.assertEqual(
    reality,
    (
        f'Hello, my name is {first_name}'
        f' {last_name} and I am'
        f' {self.calculate_age(year_of_birth)}.'
    )
)
an_instance = src.person.Person(
    first_name='first_name',
    last_name='last_name',
    sex='M',
    year_of_birth=row[0],
    passed_test=row[1],
)
self.assertEqual(an_instance.can_vote(), row[2])
self.assertEqual(an_instance.can_get_license(), row[-1])

I can make one function with a for loop to remove the repetition.


RED: make it fail


I add a test with the code from test_say_hello_method for the Person object

23  def calculate_age(self, year_of_birth):
24      return self.this_year - year_of_birth
25
26  def test_person_object(self):
27      for a_person in self.people:
28          first_name = a_person[0]
29          last_name = a_person[1]
30          sex = a_person[2]
31          year_of_birth = a_person[3]
33          with self.subTest(
34              first_name=first_name,
35              year_of_birth=year_of_birth,
36          ):
37              try:
38                  an_instance = src.person.Person(
39                      first_name=first_name,
40                      last_name=last_name,
41                      sex=sex,
42                      year_of_birth=year_of_birth,
43                  )
44              except TypeError as error:
45                  self.assertEqual(
46                      error.args[0],
47                      f"'{year_of_birth}' is not an integer"
48                  )
49              else:
50                  self.assertEqual(
51                      an_instance.boom(),
52                      (
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                  )
58
59  def test_say_hello_method(self):

the terminal is my friend, and shows AttributeError

SUBFAILED(first_name='jane', year_of_birth=1991)
    AttributeError: 'Person' object has no attribute 'boom'
SUBFAILED(first_name='joe', year_of_birth=1996)
    AttributeError: 'Person' object has no attribute 'boom'
SUBFAILED(first_name='mary', year_of_birth=2000)
    AttributeError: 'Person' object has no attribute 'boom'
SUBFAILED(first_name='john', year_of_birth=1980)
    AttributeError: 'Person' object has no attribute 'boom'

GREEN: make it pass


I change boom to say_hello in the assertion

49                else:
50                    self.assertEqual(
51                        an_instance.say_hello(),
52                        (
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                    )
58
59    def test_say_hello_method(self):

the test passes.


REFACTOR: make it better


  • I remove test_say_hello_method because it is now a repetition

  • I add an assertion for the can_vote method to test_person_object

    the terminal is my friend, and shows AssertionError

    SUBFAILED(first_name='jane', year_of_birth=1991)
        AssertionError: True != False
    SUBFAILED(first_name='joe', year_of_birth=1996)
        AssertionError: True != False
    SUBFAILED(first_name='mary', year_of_birth=2000)
        AssertionError: True != False
    SUBFAILED(first_name='john', year_of_birth=1980)
        AssertionError: True != False
    
  • I add values for is_citizen to the people object attribute of the TestPerson object

  • I use the values for the is_citizen parameter when making an instance of the Person object in test_person_object

    the terminal still shows AssertionError

  • I change the expectation of the assertion for the can_vote method to use the value of the is_citizen variable

    the test passes.

  • I add an assertion for the can_get_license method to test_person_object

    the terminal is my friend, and shows AssertionError

    SUBFAILED(first_name='jane', year_of_birth=1991)
        AssertionError: False != True
    SUBFAILED(first_name='joe', year_of_birth=1996)
        AssertionError: False != True
    SUBFAILED(first_name='mary', year_of_birth=2000)
        AssertionError: False != True
    SUBFAILED(first_name='john', year_of_birth=1980)
        AssertionError: False != True
    
  • I add values for passed_test to the people object attribute of the TestPerson object

  • I use the values for the passed_test parameter when making an instance of the Person object in test_person_object

    the terminal still shows AssertionError

  • I change the expectation of the assertion for the can_get_license method to use the value of the passed_test variable

    the test passes.

  • test_person_object is missing the cases for if a person younger than 18

    age < 18

    is citizen

    can vote

    True

    True

    False

    True

    False

    False

    age < 18

    passed test

    can get license

    True

    True

    False

    True

    False

    False

    which gives this truth table when put together

    age < 18

    is citizen

    passed_test

    can vote

    can_get_license

    True

    True

    True

    False

    False

    True

    False

    True

    False

    False

    True

    True

    False

    False

    False

    True

    False

    False

    False

    False

    I add tuples for these cases to the people object attribute

    the terminal is my friend, and shows AssertionError

    SUBFAILED(first_name='first_name', year_of_birth=YYYY) ...
        AssertionError: False != True
    SUBFAILED(first_name='first_name', year_of_birth=YYYY) ...
        AssertionError: False != True
    SUBFAILED(first_name='first_name', year_of_birth=YYYY) ...
        AssertionError: False != True
    

    I need a better error message, so I know which tuples caused the failures.

  • I change the names in the subTest method to show the entire tuple in test_person_object

    the terminal shows

    SUBFAILED[('first_name', 'last_name', 'M', YYYY, True, True)] ...
        AssertionError: False != True
    SUBFAILED[('first_name', 'last_name', 'M', YYYY, False, True)] ...
        AssertionError: False != True
    SUBFAILED[('first_name', 'last_name', 'M', YYYY, True, False)] ...
        AssertionError: False != True
    

    because I used the value for the is_citizen variable as the expectation of the assertion for the can_vote method which is wrong when the person is younger than 18, the expectation should always be False.

  • I add the expectation for all the cases of the can_vote method to the people object attribute

  • I remove the is_citizen variable then change the expectation of the assertion for the can_vote method in test_person_object to the new item I added to the tuples in the people object attribute

    the terminal is my friend, and shows AssertionError

    SUBFAILED[('first_name', 'last_name', 'M', YYYY, True, True, False)] ...
        AssertionError: False != True
    SUBFAILED[('first_name', 'last_name', 'M', YYYY, False, True, False)] ...
        AssertionError: False != True
    

    because I used the value for the passed_test variable as the expectation of the assertion for the can_get_license method which is wrong when the person is younger than 18, the expectation should always be False.

  • I add the expectation for all the cases of the can_get_license method to the people object attribute

  • I remove the passed_test variable then change the expectation of the assertion for the can_get_license method in test_person_object to the new item I added to the tuples in the people object attribute

    the test passes.

  • I remove the commented lines from test_person_object

  • I remove test_if_person_can_vote because it is now a repetition

  • I remove test_if_person_can_get_license because it is now a repetition

  • I remove the truth_table object_attribute because it is no longer used

  • I add a git commit message

    terminal
    git commit -am 'extract test_person_object'
    

extract object attributes

The people object attribute can be described with the following truth table

  • If a person is younger than 18

    age < 18

    is citizen

    passed_test

    can vote

    can_get_license

    True

    True

    True

    False

    False

    True

    False

    True

    False

    False

    True

    True

    False

    False

    False

    True

    False

    False

    False

    False

  • If a person is NOT younger than 18

    age < 18

    is citizen

    passed_test

    can vote

    can_get_license

    person

    False

    True

    True

    True

    True

    jane

    False

    False

    True

    False

    True

    mary

    False

    True

    False

    True

    False

    joe

    False

    False

    False

    False

    False

    john

  • I make an object attribute for datetime.date.today().year-17

  • I change the comments then use the new object attribute for datetime.date.today().year-17 from the people object attribute

    the tests are still green.

  • I add a new object attribute for if a person is NOT younger than 18

  • I add comments then use the new object attribute for the year_of_birth values of jane, joe, mary and john in the people object attribute

    still green.

  • I change the values for first_name, last_name and sex to 'first_name', 'last_name', 'M', for all the tuples in the people object attribute

    green.

  • I add object attributes for first_name, last_name and sex

  • I use the new object attributes for first_name, last_name and sex, move them out of the for loop in test_person_object

    still green.

  • I use the new object attributes for first_name and last_name, then move them out of the for loop in test_say_hello_function

    the test is still green.

  • I use the new object attributes for first_name, last_name and sex, then move them out of the for loop in test_person_factory

    still green.

  • I use the object attributes directly in test_person_factory (I do not need the variables)

    green.

  • I use the object attributes directly in test_say_hello_function

  • I use the object attributes directly in test_person_object

  • I remove the values of first_name, last_name amd sex from the tuples in the people object attribute

    the terminal is my friend, and shows IndexError and ValueError

    SUBFAILED[(2008, True, True, True, True)] ...test_person_object -
        IndexError: tuple index out of range
    SUBFAILED[(2008, True, True, True, True)] ...test_say_hello_function -
        ValueError
    

    because the positions changed when I changed the tuple. I would have to change all the indexes if I change the tuple, there has to be a better way

  • I undo the change

    the tests are green again.

  • I change the values for the first three items of the tuples in the people object attribute to the year_of_birth value and the boolean for is_citizen and passed_test

    the tests are still green.

  • I change the indexes for year_of_birth, is_citizen and passed_test in test_person_object

    still green.

  • I change the index for year_of_birth in test_say_hello_function

    green.

  • I change the index for year_of_birth in test_person_factory

    still green.

  • I remove the repeated values from the tuples in the people object attribute

    the terminal is my friend, and shows IndexError

    SUBFAILED[(2008, True, True, True, True)] ...test_person_object -
        IndexError: tuple index out of range
    SUBFAILED[(2008, True, False, True, False)] ...test_person_object -
        IndexError: tuple index out of range
    SUBFAILED[(2008, False, True, False, True)] ...test_person_object -
        IndexError: tuple index out of range
    SUBFAILED[(2008, False, False, False, False)] ...test_person_object -
        IndexError: tuple index out of range
    SUBFAILED[(2009, True, True, False, False)] ...test_person_object -
        IndexError: tuple index out of range
    SUBFAILED[(2009, False, True, False, False)] ...test_person_object -
        IndexError: tuple index out of range
    SUBFAILED[(2009, True, False, False, False)] ...test_person_object -
        IndexError: tuple index out of range
    SUBFAILED[(2009, False, False, False, False)] ...test_person_object -
        IndexError: tuple index out of range
    
  • I change the indexes for the expectations of the results to calls of the can_vote and can_get_license methods in test_person_object

    the test is green again.

  • I add a git commit message

    terminal
    git commit -am 'extract object attributes'
    

use assertRaises with test_when_person_is_older_than_120

RED: make it fail


I use the assertRaises method to replace the try statement in test_when_person_is_older_than_120

143    def test_when_person_is_older_than_120(self):
144        # try:
145        with self.assertRaises(TypeError):
146            src.person.Person(
147                first_name='first_name',
148                last_name='last_name',
149                sex='M',
150                year_of_birth=self.this_year-121
151            )
152        # except ValueError:
153        #     pass
154
155    def test_when_year_of_birth_is_the_future(self):

the terminal is my friend, and shows ValueError

E           ValueError

GREEN: make it pass


I change the Exception to match

143    def test_when_person_is_older_than_120(self):
144        # try:
145        with self.assertRaises(ValueError):

the test passes.


REFACTOR: make it better



use assertRaises with test_when_year_of_birth_is_the_future

RED: make it fail


I use the assertRaisesRegex method to replace the try statement in test_when_year_of_birth_is_the_future

158    def test_when_year_of_birth_is_the_future(self):
159        # try:
160        with self.assertRaisesRegex(
161            ValueError, 'BOOM!!!'
162        ):
163            src.person.Person(
164                first_name='first_name',
165                last_name='last_name',
166                sex='F',
167                year_of_birth=self.this_year+1,
168            )
169        # except ValueError:
170        #     pass
171
172    def test_when_year_of_birth_is_not_an_integer(self):

the terminal is my friend, and shows AssertionError

AssertionError:
    "BOOM!!!" does not match
    "a -1 year old person is too old to be alive."

GREEN: make it pass


I change the Exception to match

158    def test_when_year_of_birth_is_the_future(self):
159        # try:
160        with self.assertRaises(ValueError):

the test passes.


REFACTOR: make it better



extract test_if_person_is_not_alive

test_when_person_is_older_than_120 and test_when_year_of_birth_is_the_future both check if ValueError is raised if year_of_birth makes the person older than 120 or younger than 0

age = self.calculate_age(year_of_birth)

with self.assertRaisesRegex(
    ValueError,
    f'a {age} year old person is not alive.'
):
    src.person.Person(
        first_name='first_name',
        last_name='last_name',
        sex='M',
        year_of_birth=year_of_birth,
    )

I can make one function with a for loop to remove the repetition.


RED: make it fail


I add a test with an assertion for if a person is born in the future or is older than 120

125      def test_person_factory(self):
126          for a_person in self.people:
127              year_of_birth = a_person[0]
128
129              with self.subTest(a_person):
130                  self.assertEqual(
131                      src.person.person(
132                          first_name=self.first_name,
133                          last_name=self.last_name,
134                          sex=self.sex,
135                          year_of_birth=year_of_birth,
136                      ),
137                      (
138                          f'{self.first_name}, {self.last_name},'
139                          f' {self.sex}, {year_of_birth}'
140                      )
141                  )
143      def test_if_person_is_not_alive(self):
144          for year_of_birth in (
145              self.this_year+1,
146              self.this_year-121,
147          ):
148              age = self.calculate_age(year_of_birth)
149
150              with self.subTest(year_of_birth):
151                  with self.assertRaisesRegex(
152                      ValueError,
153                      'BOOM!!!'
154                  ):
155                      src.person.Person(
156                          first_name=self.first_name,
157                          last_name=self.last_name,
158                          sex=self.sex,
159                          year_of_birth=year_of_birth,
160                      )
161
162      def test_when_person_is_older_than_120(self):

the terminal is my friend, and shows AssertionError

SUBFAILED(year_of_birth=XXXX) ...AssertionError:
    "BOOM!!!" does not match "a -1 year old person is not alive."
SUBFAILED(year_of_birth=YYYY) ...AssertionError:
    "BOOM!!!" does not match "a 121 year old person is not alive."

GREEN: make it pass


I change the error message in extract test_if_person_is_not_alive to match

143      def test_if_person_is_not_alive(self):
144          for year_of_birth in (
145              self.this_year+1,
146              self.this_year-121,
147          ):
148              age = self.calculate_age(year_of_birth)
149
150              with self.subTest(year_of_birth):
151                  with self.assertRaisesRegex(
152                      ValueError,
153                      f'a {age} year old person is not alive.'
154                  ):
155                      src.person.Person(
156                          first_name=self.first_name,
157                          last_name=self.last_name,
158                          sex=self.sex,
159                          year_of_birth=year_of_birth,
160                      )

the test passes.


REFACTOR: make it better



move test_when_year_of_birth_is_not_an_integer to test_person_object

test_when_year_of_birth_is_not_an_integer has multiple try statements for if year_of_birth is not an integer. It is also the last tuple in the people object attribute - its year_of_birth values are not integers. I can use the object attributes to remove the repetition

  • I add tuples for the cases in test_when_year_of_birth_is_not_an_integer to the people object attribute

    the tests are still green.

  • I change the expected Exception in test_person_object to test my change

    the terminal is my friend, and shows TypeError

    SUBFAILED[(None, False, False)] ...test_person_object -
        TypeError: 'None' is not an integer
    SUBFAILED[(2026.0, False, False)] ...test_person_object -
        TypeError: '2026.0' is not an integer
    SUBFAILED[('2026', False, False)] ...test_person_object -
        TypeError: '2026' is not an integer
    SUBFAILED[((2026,), False, False)] ...test_person_object -
        TypeError: '(2026,)' is not an integer
    SUBFAILED[('a_string', False, False, False, False)] ...test_person_object -
        TypeError: 'a_string' is not an integer
    

    good, the test works.

  • I undo the change

    the test is green again.

  • I remove the last tuple from the people object attribute because it is a repetition of when year_of_birth is a string (('2026', False, False,),)

  • I remove test_when_year_of_birth_is_not_an_integer because it is now covered by test_person_object

  • I add a git commit message

    terminal
    git commit -am \
    'move test_when_year_of_birth_is_not_an_integer to test_person_object'
    

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

    terminal
    cd ..
    

review

I can use for loops for things that repeat.

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 AssertionError with loops?


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.