how to make a person with generators
I have a problem in person/tests/test_person.py
test_joe, test_jane, test_john and test_mary have the same assertions.
test_when_year_of_birth_is_not_an_integer also has assertions that are basically the same, the only thing that changes are the values for the
year_of_birthparameter.
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
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'
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 ),
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 ),
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
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
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 )
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
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 )
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 )
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 )
open the project
I open a terminal
I change directory to the project
I open
test_person.pyfrom thetestsfolderI use pytest-watcher to run the tests automatically
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.pythe test is still green.
I made a tuple named
peoplethat contains a tuple forjoepeople = ( ('joe', 'blow', 'M', 1996), )I use a for loop to repeat the same commands for each item in the
peopletuple, in this case there is only one item - the tuple forjoefor a_person in ( ('joe', 'blow', 'M', 1996), ):I use the index of each item in
joefor the parameters when the test calls the person functionfor 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
janeto the tuple ofpeoplestill 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
marystill 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
johngreen, showing that for each tuple in the
peopletuple, 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_birththe 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' != NoneI 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
peopletuple ('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
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_birthis not an integer.year_of_birthis a string in the last tuple in thepeopletuple (('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
I add a message to the calculate_age function for when TypeError is raised, in
src/person/__init__.pythe terminal is my friend, and shows TypeError
TypeError: 'a string' is not an integerbetter.
I change the statements in the assert_say_hello_works method to a try statement with an else clause in
tests/test_person.pythe test passes.
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,argsandwith_traceback.I change the assertion to see what
add_noteisthe terminal is my friend, and shows AssertionError
AssertionError: <built-in method add_note of TypeError object at 0xffff7ed65c43> != Nonebecause
add_noteis a 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
argsisthe terminal is my friend, and shows AssertionError
AssertionError: ("'a string' is not an integer",) != Nonefantastic!
argsis 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__.pyto test my changethe 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.' != NoneI 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
peopletuple ('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
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 integerbetter.
SUBFAILED(first_name='first_name')shows the value I gave inwith 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_birthto the call to the subTest methodthe 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 integerthis is clearer.
GREEN: make it pass
I move the statements in the assert_person_can_say_hello method to a try…except…else clause like I did for test_say_hello_function
the test passes.
REFACTOR: make it better
I change the error message for if the value of the
year_of_birthparameter is not an integer in the calculate_age function, insrc/person/__init__.pythe 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.pythe 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_birthparameter is not an integer back to the correct message, insrc/person/__init__.pythe 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_birthSUBFAILED(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.' != NoneI 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
peopletuple.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
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.
I add an object attribute for the
peopletuple to the TestPerson objectI use the object attribute for
peoplein the for loop in test_say_hello_methodthe test is still green.
I remove the commented lines from test_say_hello_method
I use the object attribute for
peoplein the for loop in test_say_hello_functionstill green.
I remove the commented lines from test_say_hello_function
I use the object attribute for
peoplein the for loop in test_person_factorythe test is still green.
I remove the commented lines from test_person_factory
I add a git commit message
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_tabletuple for if a person is younger than18AND is NOT a citizenage < 18
is citizen
can vote
True
False
False
the test is still green.
I add a tuple to the
truth_tabletuple for if a person is NOT younger than18AND is a citizenage < 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 != FalseI add the expectation to the tuples for the rows in the
truth_tabletupleI 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 rangeI change the index to the right number
the test passes.
I add a tuple to the
truth_tabletuple for if a person is NOT younger than18AND is NOT a citizenage < 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
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_tabletuple for if a person is NOT younger than18AND has passed the testage < 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 != FalseI use the index to change the expectation to the last item of the
rowtuplethe test passes.
I add a tuple to the
truth_tabletuple for if a person is younger than18AND has NOT passed the testage < 18
passed test
can get license
True
False
False
the test is still green.
I add a tuple to the
truth_tabletuple for if a person is younger than18AND has passed the testage < 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
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.
I add an object attribute for the
truth_tabletuple to the TestPerson objectI use the object attribute for
truth_tablein the for loop in test_if_person_can_votethe test is still green.
I remove the commented lines from test_if_person_can_vote
I use the object attribute for
truth_tablein the for loop in test_if_person_can_get_licensestill green.
I remove the commented lines from test_if_person_can_vote
I add a git commit message
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 != FalseI add values for
is_citizento thepeopleobject attribute of the TestPerson objectI use the values for the
is_citizenparameter when making an instance of the Person object in test_person_objectthe terminal still shows AssertionError
I change the expectation of the assertion for the can_vote method to use the value of the
is_citizenvariablethe 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 != TrueI add values for
passed_testto thepeopleobject attribute of the TestPerson objectI use the values for the
passed_testparameter when making an instance of the Person object in test_person_objectthe terminal still shows AssertionError
I change the expectation of the assertion for the can_get_license method to use the value of the
passed_testvariablethe test passes.
test_person_object is missing the cases for if a person younger than
18age < 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
peopleobject attributethe 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 != TrueI 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 != Truebecause I used the value for the
is_citizenvariable as the expectation of the assertion for the can_vote method which is wrong when the person is younger than18, the expectation should always be False.I add the expectation for all the cases of the can_vote method to the
peopleobject attributeI remove the
is_citizenvariable 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 thepeopleobject attributethe 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 != Truebecause I used the value for the
passed_testvariable as the expectation of the assertion for the can_get_license method which is wrong when the person is younger than18, the expectation should always be False.I add the expectation for all the cases of the can_get_license method to the
peopleobject attributeI remove the
passed_testvariable 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 thepeopleobject attributethe 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_tableobject_attribute because it is no longer usedI add a git commit message
extract object attributes
The people object attribute can be described with the following truth table
If a person is younger than
18age < 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
18age < 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-17I change the comments then use the new object attribute for
datetime.date.today().year-17from thepeopleobject attributethe tests are still green.
I add a new object attribute for if a person is NOT younger than
18I add comments then use the new object attribute for the
year_of_birthvalues ofjane,joe,maryandjohnin thepeopleobject attributestill green.
I change the values for
first_name,last_nameandsexto'first_name', 'last_name', 'M',for all the tuples in thepeopleobject attributegreen.
I add object attributes for
first_name,last_nameandsexI use the new object attributes for
first_name,last_nameandsex, move them out of the for loop in test_person_objectstill green.
I use the new object attributes for
first_nameandlast_name, then move them out of the for loop in test_say_hello_functionthe test is still green.
I use the new object attributes for
first_name,last_nameandsex, then move them out of the for loop in test_person_factorystill 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_nameamdsexfrom the tuples in thepeopleobject attributethe 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 - ValueErrorbecause 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
peopleobject attribute to theyear_of_birthvalue and the boolean foris_citizenandpassed_testthe tests are still green.
I change the indexes for
year_of_birth,is_citizenandpassed_testin test_person_objectstill green.
I change the index for
year_of_birthin test_say_hello_functiongreen.
I change the index for
year_of_birthin test_person_factorystill green.
I remove the repeated values from the tuples in the
peopleobject attributethe 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 rangeI 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
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
I change assertRaises to assertRaisesRegex to test the message of the Exception
the terminal is my friend, and shows AssertionError
AssertionError: "a 121 year old person is too old to be alive." does not match ""I add a message to the Exception in the calculate_age function in
src/person/__init__.pythe test passes.
I remove the commented lines from test_when_person_is_older_than_120 in
tests/test_person.pyI add a git commit message
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
I change assertRaises to assertRaisesRegex to test the message of the Exception
the terminal is my friend, and shows AssertionError
AssertionError: "BOOM!!!" does not match "a -1 year old person is too old to be alive."I need a better message for if a person is too old to be alive or too young to be alive.
I change the message of the Exception in the calculate_age function in
src/person/__init__.pythe terminal is my friend, and shows AssertionError
FAILED ...test_when_person_is_older_than_120 - AssertionError: "a 121 year old person is too old to be alive." does not match "a 121 year old person is not alive." FAILED ...test_when_year_of_birth_is_the_future - AssertionError: "BOOM!!!" does not match "a -1 year old person is not alive."I change the error message in test_when_person_is_older_than_120 in
tests/test_person.pythe terminal still shows AssertionError
I change the error message in test_when_year_of_birth_is_the_future
the test passes.
I remove the commented lines from test_when_year_of_birth_is_the_future
I add a git commit message
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
I put the two with statements together
the test is still green.
I remove test_when_person_is_older_than_120 because it is now covered by test_if_person_is_not_alive
I remove test_when_year_of_birth_is_the_future because it is now covered by test_if_person_is_not_alive
I add a git commit message
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
peopleobject attributethe 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 integergood, the test works.
I undo the change
the test is green again.
I remove the last tuple from the
peopleobject attribute because it is a repetition of whenyear_of_birthis 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
close the project
I close
test_person.pyandsrc/person/__init__.pyI 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
review
I can use for loops for things that repeat.
My tests still have problems:
The attribute tests - test_dir_person_object and test_dir_person_instance catch changes to the attributes and methods of the Person object and they are a problem to maintain. There has to be a better way.
The
peopleobject attribute is confusing, anyone who reads it without going through the process of making it would not know what the booleans stands for and the last few tuples I added are a different length than the others. There has to be a better way.
code from the chapter
what is next?
I know how to make a Python Test Driven Development environment manually.
I know how to make a Python Test Driven Development environment automatically.
I know how to make a Python Test Driven Development environment automatically with variables.
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.