how to make a person with exceptions
I want to be able to check if a person can vote, and if they can get a license. In other words, I want something in the person project to make decisions based on conditions. For example
If a person is younger than
18the person cannot get a license.
the person cannot vote.
If a person is
18or olderand passes a test, the person can get a license.
and is a citizen, the person can vote.
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 @staticmethod
9 def calculate_age(year_of_birth):
10 return (
11 datetime.date.today().year
12 - year_of_birth
13 )
14
15 def test_joe(self):
16 first_name = 'joe'
17 last_name = 'blow'
18 sex = 'M'
19 year_of_birth = 1996
20
21 reality = src.person.factory(
22 first_name=first_name,
23 last_name=last_name,
24 sex=sex,
25 year_of_birth=year_of_birth,
26 )
27 my_expectation = (
28 f'{first_name}, {last_name},'
29 f' {sex}, {year_of_birth}'
30 )
31 assert reality == my_expectation
32 self.assertEqual(reality, my_expectation)
33
34 reality = src.person.say_hello(
35 first_name=first_name,
36 last_name=last_name,
37 year_of_birth=year_of_birth,
38 )
39 my_expectation = (
40 f'Hello, my name is {first_name}'
41 f' {last_name} and I am'
42 f' {self.calculate_age(year_of_birth)}.'
43 )
44 assert reality == my_expectation
45 self.assertEqual(reality, my_expectation)
46
47 joe = src.person.Person(
48 first_name=first_name,
49 last_name=last_name,
50 sex=sex,
51 year_of_birth=year_of_birth,
52 )
53
54 reality = joe.say_hello()
55 assert reality == my_expectation
56 self.assertEqual(reality, my_expectation)
57 self.assertEqual(joe.can_vote(), True)
58 self.assertEqual(joe.can_get_license(), False)
59
60 def test_jane(self):
61 first_name = 'jane'
62 last_name = 'doe'
63 sex = 'F'
64 year_of_birth = 1991
65
66 reality = src.person.factory(
67 first_name=first_name,
68 last_name=last_name,
69 sex=sex,
70 year_of_birth=year_of_birth,
71 )
72 my_expectation = (
73 f'{first_name}, {last_name},'
74 f' {sex}, {year_of_birth}'
75 )
76 assert reality == my_expectation
77 self.assertEqual(reality, my_expectation)
78
79 reality = src.person.say_hello(
80 first_name=first_name,
81 last_name=last_name,
82 year_of_birth=year_of_birth,
83 )
84 my_expectation = (
85 f'Hello, my name is {first_name}'
86 f' {last_name} and I am'
87 f' {self.calculate_age(year_of_birth)}.'
88 )
89 assert reality == my_expectation
90 self.assertEqual(reality, my_expectation)
91
92 jane = src.person.Person(
93 first_name=first_name,
94 last_name=last_name,
95 sex=sex,
96 year_of_birth=year_of_birth,
97 passed_test=True,
98 )
99
100 reality = jane.say_hello()
101 assert reality == my_expectation
102 self.assertEqual(reality, my_expectation)
103 self.assertEqual(jane.can_vote(), True)
104 self.assertEqual(jane.can_get_license(), True)
105
106 def test_john(self):
107 first_name = 'john'
108 last_name = 'smith'
109 sex = 'M'
110 year_of_birth = 1980
111 # year_of_birth = 1580
112 # raises AssertionError
113 # because older than 120
114
115 reality = src.person.factory(
116 first_name=first_name,
117 last_name=last_name,
118 sex=sex,
119 year_of_birth=year_of_birth,
120 )
121 my_expectation = (
122 f'{first_name}, {last_name},'
123 f' {sex}, {year_of_birth}'
124 )
125 assert reality == my_expectation
126 self.assertEqual(reality, my_expectation)
127
128 reality = src.person.say_hello(
129 first_name=first_name,
130 last_name=last_name,
131 year_of_birth=year_of_birth,
132 )
133 my_expectation = (
134 f'Hello, my name is {first_name}'
135 f' {last_name} and I am'
136 f' {self.calculate_age(year_of_birth)}.'
137 )
138 assert reality == my_expectation
139 self.assertEqual(reality, my_expectation)
140
141 john = src.person.Person(
142 first_name=first_name,
143 last_name=last_name,
144 sex=sex,
145 year_of_birth=year_of_birth,
146 is_citizen=False,
147 )
148
149 reality = john.say_hello()
150 assert reality == my_expectation
151 self.assertEqual(reality, my_expectation)
152 self.assertEqual(john.can_vote(), False)
153 self.assertEqual(john.can_get_license(), False)
154
155 def test_mary(self):
156 first_name = 'mary'
157 last_name = 'public'
158 sex = 'F'
159 year_of_birth = 2000
160
161 reality = src.person.factory(
162 first_name=first_name,
163 last_name=last_name,
164 sex=sex,
165 year_of_birth=year_of_birth,
166 )
167 my_expectation = (
168 f'{first_name}, {last_name},'
169 f' {sex}, {year_of_birth}'
170 )
171 assert reality == my_expectation
172 self.assertEqual(reality, my_expectation)
173
174 reality = src.person.say_hello(
175 first_name=first_name,
176 last_name=last_name,
177 year_of_birth=year_of_birth,
178 )
179 my_expectation = (
180 f'Hello, my name is {first_name}'
181 f' {last_name} and I am'
182 f' {self.calculate_age(year_of_birth)}.'
183 )
184 assert reality == my_expectation
185 self.assertEqual(reality, my_expectation)
186
187 mary = src.person.Person(
188 first_name=first_name,
189 last_name=last_name,
190 sex=sex,
191 year_of_birth=year_of_birth,
192 is_citizen=False,
193 passed_test=True,
194 )
195
196 reality = mary.say_hello()
197 assert reality == my_expectation
198 self.assertEqual(reality, my_expectation)
199 self.assertEqual(mary.can_vote(), False)
200 self.assertEqual(mary.can_get_license(), True)
201
202 def test_underage_citizen(self):
203 person = src.person.Person(
204 first_name='first_name',
205 last_name='last_name',
206 sex='M',
207 year_of_birth=datetime.date.today().year-17,
208 is_citizen=True,
209 passed_test=True,
210 )
211 self.assertEqual(person.can_vote(), False)
212 self.assertEqual(
213 person.can_get_license(), False
214 )
215
216 @unittest.skip('will always fail')
217 def test_when_year_of_birth_is_not_an_integer(self):
218 person = src.person.Person(
219 first_name='first_name',
220 last_name='last_name',
221 sex='M',
222 # year_of_birth=None, # fails
223 # year_of_birth=False, # fails
224 # year_of_birth=2026.0, # fails
225 # year_of_birth='2026', # fails
226 # year_of_birth=(2026,), # fails
227 )
228 # person.say_hello()
229 # fails if year_of_birth is not an integer
230
231 def test_dir_person_class(self):
232 reality = dir(src.person.Person)
233 my_expectation = [
234 '__class__',
235 '__delattr__',
236 '__dict__',
237 '__dir__',
238 '__doc__',
239 '__eq__',
240 '__firstlineno__',
241 '__format__',
242 '__ge__',
243 '__getattribute__',
244 '__getstate__',
245 '__gt__',
246 '__hash__',
247 '__init__',
248 '__init_subclass__',
249 '__le__',
250 '__lt__',
251 '__module__',
252 '__ne__',
253 '__new__',
254 '__reduce__',
255 '__reduce_ex__',
256 '__repr__',
257 '__setattr__',
258 '__sizeof__',
259 '__static_attributes__',
260 '__str__',
261 '__subclasshook__',
262 '__weakref__',
263 'can_get_license',
264 'can_vote',
265 'check_age',
266 'say_hello',
267 ]
268 assert reality == my_expectation
269 self.assertEqual(reality, my_expectation)
270
271 def test_dir_person_instance(self):
272 an_instance_of_person = src.person.Person(
273 first_name='first_name',
274 last_name='last_name',
275 sex='M',
276 year_of_birth=2026,
277 )
278
279 reality = dir(an_instance_of_person)
280 my_expectation = [
281 '__class__',
282 '__delattr__',
283 '__dict__',
284 '__dir__',
285 '__doc__',
286 '__eq__',
287 '__firstlineno__',
288 '__format__',
289 '__ge__',
290 '__getattribute__',
291 '__getstate__',
292 '__gt__',
293 '__hash__',
294 '__init__',
295 '__init_subclass__',
296 '__le__',
297 '__lt__',
298 '__module__',
299 '__ne__',
300 '__new__',
301 '__reduce__',
302 '__reduce_ex__',
303 '__repr__',
304 '__setattr__',
305 '__sizeof__',
306 '__static_attributes__',
307 '__str__',
308 '__subclasshook__',
309 '__weakref__',
310 'age',
311 'can_get_license',
312 'can_vote',
313 'check_age',
314 'first_name',
315 'is_citizen',
316 'last_name',
317 'passed_test',
318 'say_hello',
319 'sex',
320 'year_of_birth',
321 ]
322 assert reality == my_expectation
323 self.assertEqual(reality, my_expectation)
324
325
326# Exceptions seen
327# AssertionError
328# NameError
329# TypeError
330# AttributeError
331# SyntaxError
open the project
I open a terminal
I change directory to the project
cd personI open
test_person.pyfrom thetestsfolderI use pytest-watcher to run the tests automatically
uv run pytest-watcher . --nowthe terminal shows
tests/test_person.py ....... [100%] =================== 7 passed in P.QRs ====================
add can_vote method
RED: make it fail
I add a call to can_vote from test_joe
54 reality = joe.say_hello()
55 assert reality == my_expectation
56 self.assertEqual(reality, my_expectation)
57 self.assertEqual(joe.can_vote(), True)
58
59 def test_jane(self):
the terminal is my friend, and shows AttributeError
AttributeError: 'Person' object
has no attribute 'can_vote'
GREEN: make it pass
I open
person/__init__.pyfrom thesrcfolderI add a function definition to the Person class in
person.py4 class Person: 5 6 def __init__( 7 self, first_name, last_name, 8 sex, year_of_birth=None, 9 ): 10 self.first_name = first_name 11 self.last_name = last_name 12 self.year_of_birth = year_of_birth 13 self.sex = sex 14 15 def can_vote(): 16 return True 17 18 def say_hello(self): 19 return ( 20 f'Hello, my name is {self.first_name}' 21 f' {self.last_name} and I am' 22 f' {calculate_age(self.year_of_birth)}.' 23 )the terminal is my friend, and shows TypeError
TypeError: Person.can_vote() takes 0 positional arguments but 1 was givenI add the staticmethod decorator
15 @staticmethod 16 def can_vote(): 17 return True 18 19 def say_hello(self):the terminal is my friend, and shows AssertionError
FAILED ...::TestPerson::test_dir_person_class - AssertionError: assert ['__class__',...'__eq__', ...] ==... FAILED ...::TestPerson::test_dir_person_instance - AssertionError: assert ['__class__',...'__eq__', ...] ==...the tests for the attributes and methods of the Person class and an instance of it are failing because I added a method to it.
I add
can_voteto test_dir_person_class237 'can_vote', 238 'say_hello', 239 ] 240 assert reality == my_expectation 241 self.assertEqual(reality, my_expectation) 242 243 def test_dir_person_instance(self):I add
can_voteto test_dir_person_instance282 'can_vote', 283 'first_name', 284 'last_name', 285 'say_hello', 286 'sex', 287 'year_of_birth', 288 ] 289 assert reality == my_expectation 290 self.assertEqual(reality, my_expectation) 291 292 293# Exceptions seenthe test passes.
These tests are good because they help document what is in the class and catch its changes immediately.
They are a problem because class attributes can change between Python versions, I have to remember the correct order of names and I am keeping two lists. There has to be a better way.
I open a new terminal then make sure I am in the
personfoldercd personI add a git commit message in the new terminal
git commit -am 'add can_vote method'
add is_citizen attribute
I want can_vote to return
False for
nothe person cannot vote if the person is not a citizen.True for
yesthe person can vote if the person is a citizen.
RED: make it fail
I go back to the terminal where the tests are running
I add a call to
can_votefrom test_jane98 reality = jane.say_hello() 99 assert reality == my_expectation 100 self.assertEqual(reality, my_expectation) 101 self.assertEqual(jane.can_vote(), True) 102 103 def test_john(self):the test is still green.
I add a call to
can_votefrom test_john145 reality = john.say_hello() 146 assert reality == my_expectation 147 self.assertEqual(reality, my_expectation) 148 self.assertEqual(john.can_vote(), False) 149 150 def test_mary(self):the terminal is my friend, and shows AssertionError
AssertionError: True != FalseThe can_vote method has to make a decision based on something.
GREEN: make it pass
I add
is_citizento the call to the Person class forjohn138 john = src.person.Person( 139 first_name=first_name, 140 last_name=last_name, 141 sex=sex, 142 year_of_birth=year_of_birth, 143 is_citizen=False, 144 ) 145 146 reality = john.say_hello() 147 assert reality == my_expectation 148 self.assertEqual(reality, my_expectation) 149 self.assertEqual(john.can_vote(), False) 150 151 def test_mary(self):the terminal is my friend, and shows TypeError
TypeError: Person.__init__() got an unexpected keyword argument 'is_citizen'because the definition for the __init__ method only takes five inputs (
self,first_name,last_name,sexandyear_of_birth) and I called it withis_citizenwhich is not one of those names.I add
is_citizento the parentheses of the __init__ method, inperson.py4class Person: 5 6 def __init__( 7 self, first_name, last_name, 8 sex, year_of_birth=None, 9 is_citizen, 10 ):the terminal is my friend, and shows SyntaxError
SyntaxError: parameter without a default follows parameter with a defaultbecause parameters without default values must come before parameters with default values.
I give
is_citizena value to make it optional4class Person: 5 6 def __init__( 7 self, first_name, last_name, 8 sex, year_of_birth=None, 9 # is_citizen, 10 is_citizen=True, 11 ):the terminal goes back to the AssertionError.
I add an instance attribute for
is_citizenso I can use it in the can_vote method4class Person: 5 6 def __init__( 7 self, first_name, last_name, 8 sex, year_of_birth=None, 9 # is_citizen, 10 is_citizen=True, 11 ): 12 self.first_name = first_name 13 self.last_name = last_name 14 self.year_of_birth = year_of_birth 15 self.sex = sex 16 self.is_citizen = is_citizen 17 18 @staticmethodstill AssertionError.
I use the class attribute in the can_vote method
18 @staticmethod 19 def can_vote(): 20 # return True 21 return self.is_citizen 22 23 def say_hello(self):the terminal is my friend, and shows NameError
NameError: name 'self' is not definedI remove the staticmethod decorator from the can_vote method then add
selfto the parentheses18 # @staticmethod 19 # def can_vote(): 20 def can_vote(self): 21 # return True 22 return self.is_citizen 23 24 def say_hello(self):the terminal is my friend, and shows AssertionError for test_dir_person_instance because I added a new attribute (
is_citizen).I add
is_citizentomy_expectationin test_dir_person_instance intest_person.py285 'can_vote', 286 'first_name', 287 'is_citizen', 288 'last_name', 289 'say_hello', 290 'sex', 291 'year_of_birth', 292 ] 293 assert reality == my_expectation 294 self.assertEqual(reality, my_expectation) 295 296 297# Exceptions seenthe test passes.
joeandjanedo not need to pass a value for theis_citizenparameter because a method uses the default value for a parameter when it is called without the parameter.
REFACTOR: make it better
I remove the commented lines from
person.py4class Person: 5 6 def __init__( 7 self, first_name, last_name, 8 sex, year_of_birth=None, 9 is_citizen=True, 10 ): 11 self.first_name = first_name 12 self.last_name = last_name 13 self.year_of_birth = year_of_birth 14 self.sex = sex 15 self.is_citizen = is_citizen 16 17 def can_vote(self): 18 return self.is_citizen 19 20 def say_hello(self):I add a call to
can_votefrom test_mary intest_person.py190 reality = mary.say_hello() 191 assert reality == my_expectation 192 self.assertEqual(reality, my_expectation) 193 self.assertEqual(mary.can_vote(), False) 194 195 def test_when_year_of_birth_is_not_an_integer(self):the terminal is my friend, and shows AssertionError
AssertionError: True != Falsebecause a method uses the default value for a parameter when it is called without the parameter.
I add
is_citizento the call to the Person class formary183 mary = src.person.Person( 184 first_name=first_name, 185 last_name=last_name, 186 sex=sex, 187 year_of_birth=year_of_birth, 188 is_citizen=False, 189 ) 190 191 reality = mary.say_hello() 192 assert reality == my_expectation 193 self.assertEqual(reality, my_expectation) 194 self.assertEqual(mary.can_vote(), False) 195 196 def test_when_year_of_birth_is_not_an_integer(self):the test passes.
I add a git commit message in the other terminal
git commit -am \ 'add is_citizen attribute'
add condition to can_vote
I want the can_vote method to use two conditions to make a decision
is the person a citizen?
is the person younger than
18?
I can do that with an if statement
RED: make it fail
I go back to the terminal where the tests are running
I add a test for a person who is a citizen and younger than 18
191 reality = mary.say_hello() 192 assert reality == my_expectation 193 self.assertEqual(reality, my_expectation) 194 self.assertEqual(mary.can_vote(), False) 195 196 def test_underage_citizen(self): 197 person = src.person.Person( 198 first_name='first_name', 199 last_name='last_name', 200 sex='M', 201 year_of_birth=datetime.date.today().year-17, 202 is_citizen=True, 203 ) 204 self.assertEqual(person.can_vote(), False) 205 206 def test_when_year_of_birth_is_not_an_integer(self):the terminal is my friend, and shows AssertionError
AssertionError: True != Falsebecause
can_votereturns the value ofis_citizen, it does not care about the age of the person.I use a calculation (
datetime.date.today().year-17) as the year of birth so that the person will always be younger than18in any year the test is run.
GREEN: make it pass
I add an if statement with a call to the calculate_age function from the can_vote method in
person.py17 def can_vote(self): 18 age = calculate_age(self.year_of_birth) 19 if age < 18: 20 return False 21 return self.is_citizen 22 23 def say_hello(self):the test passes because this happens when
if age < 18:runs, Python checks ifagewhich is the result ofcalculate_age(self.year_of_birth)is less than18If
ageis greater than or equal to18, it leaves the if statement and continues to run the rest of the method -return self.is_citizen, which returnsFalse as the output if the person is not a citizen
self.is_citizen = False age >= 18 person.can_vote └──def can_vote(self): ├── if age < 18: │ return False └── return self.is_citizenTrue as the output, if the person is a citizen
self.is_citizen = True age >= 18 person.can_vote └──def can_vote(self): ├── if age < 18: │ return False └── return self.is_citizen
then leaves the function since the return statement is the last thing to run in a function.
If
ageis less than18, it goes to the next line -return False, which returns False as the output, then leaves the function since the return statement is the last thing to run in a function.self.is_citizen = False age < 18 person.can_vote └──def can_vote(self): └── if age < 18: └── return False return self.is_citizenself.is_citizen = True age < 18 person.can_vote └──def can_vote(self): └── if age < 18: └── return False return self.is_citizenI add a git commit message in the other terminal
git commit -am \ 'add condition to can_vote'
add can_get_license method
RED: make it fail
I add a call to can_get_license from test_joe
54 reality = joe.say_hello()
55 assert reality == my_expectation
56 self.assertEqual(reality, my_expectation)
57 self.assertEqual(joe.can_vote(), True)
58 self.assertEqual(joe.can_get_license(), False)
59
60 def test_jane(self):
the terminal is my friend, and shows AttributeError
AttributeError: 'Person' object
has no attribute 'can_get_license'
GREEN: make it pass
I add a method definition to the Person class in
person.py4class Person: 5 6 def __init__( 7 self, first_name, last_name, 8 sex, year_of_birth=None, 9 is_citizen=True, 10 ): 11 self.first_name = first_name 12 self.last_name = last_name 13 self.year_of_birth = year_of_birth 14 self.sex = sex 15 self.is_citizen = is_citizen 16 17 def can_get_license(): 18 return False 19 20 def can_vote(self):the terminal is my friend, and shows TypeError
TypeError: Person.can_get_license() takes 0 positional arguments but 1 was givenI add the staticmethod decorator
17 @staticmethod 18 def can_get_license(): 19 return False 20 21 def can_vote(self):the terminal is my friend, and shows AssertionError for test_dir_person_class and test_dir_person_instance.
I add
can_get_licenseto test_dir_person_class intest_person.py254 'can_get_license', 255 'can_vote', 256 'say_hello' 257 ] 258 assert reality == my_expectation 259 self.assertEqual(reality, my_expectation) 260 261 def test_dir_person_instance(self):I add
can_get_licenseto test_dir_person_instance299 'can_get_license', 300 'can_vote', 301 'first_name', 302 'is_citizen', 303 'last_name', 304 'say_hello', 305 'sex', 306 'year_of_birth', 307 ] 308 assert reality == my_expectation 309 self.assertEqual(reality, my_expectation) 310 311 312# Exceptions seenthe test passes.
I add a git commit message in the other terminal
git commit -am \ 'add can_get_license method'
add passed_test attribute
I want can_get_license to return
False for
nothe person cannot get a license if the person did not pass the test.True for
yesthe person can get a license if the person passed the test.
RED: make it fail
I add a call to can_get_license from test_mary
192 reality = mary.say_hello()
193 assert reality == my_expectation
194 self.assertEqual(reality, my_expectation)
195 self.assertEqual(mary.can_vote(), False)
196 self.assertEqual(mary.can_get_license(), True)
197
198 def test_underage_citizen(self):
the terminal is my friend, and shows AssertionError
AssertionError: False != True
GREEN: make it pass
I add
passed_testto the call to the Person class formary184 mary = src.person.Person( 185 first_name=first_name, 186 last_name=last_name, 187 sex=sex, 188 year_of_birth=year_of_birth, 189 is_citizen=False, 190 passed_test=True, 191 ) 192 193 reality = mary.say_hello() 194 assert reality == my_expectation 195 self.assertEqual(reality, my_expectation) 196 self.assertEqual(mary.can_vote(), False) 197 self.assertEqual(mary.can_get_license(), True) 198 199 def test_underage_citizen(self):the terminal is my friend, and shows TypeError
TypeError: Person.__init__() got an unexpected keyword argument 'passed_test'because the definition for the __init__ method only takes six inputs (
self,first_name,last_name,sex,year_of_birthandis_citizen). I called it withpassed_testwhich is not one of those names.I add
passed_testto the parentheses of the __init__ method, inperson.py4class Person: 5 6 def __init__( 7 self, first_name, last_name, 8 sex, year_of_birth=None, 9 is_citizen=True, 10 passed_test, 11 ):the terminal is my friend, and shows SyntaxError
SyntaxError: parameter without a default follows parameter with a defaultbecause parameters without default values must come before parameters with default values.
I give
passed_testa value to make it optional4class Person: 5 6 def __init__( 7 self, first_name, last_name, 8 sex, year_of_birth=None, 9 is_citizen=True, 10 # passed_test, 11 passed_test=False, 12 ):the terminal goes back to the AssertionError.
I add an instance attribute for
passed_testso I can use it in the can_get_license method4class Person: 5 6 def __init__( 7 self, first_name, last_name, 8 sex, year_of_birth=None, 9 is_citizen=True, 10 # passed_test, 11 passed_test=False, 12 ): 13 self.first_name = first_name 14 self.last_name = last_name 15 self.year_of_birth = year_of_birth 16 self.sex = sex 17 self.is_citizen = is_citizen 18 self.passed_test = passed_test 19 20 @staticmethodthe terminal still shows AssertionError.
I use
self.passed_testin the can_get_license method20 @staticmethod 21 def can_get_license(): 22 # return False 23 return self.passed_test 24 25 def can_vote(self):the terminal is my friend, and shows NameError
NameError: name 'self' is not definedI remove the staticmethod decorator from the can_get_license method then add
selfto the parentheses20 # @staticmethod 21 # def can_get_license(): 22 def can_get_license(self): 23 # return False 24 return self.passed_test 25 26 def can_vote(self):the terminal is my friend, and shows AssertionError for test_dir_person_instance because I added a new attribute (
passed_test).I add
passed_testtomy_expectationin test_dir_person_instance intest_person.py301 'can_get_license', 302 'can_vote', 303 'first_name', 304 'is_citizen', 305 'last_name', 306 'passed_test', 307 'say_hello', 308 'sex', 309 'year_of_birth', 310 ] 311 assert reality == my_expectation 312 self.assertEqual(reality, my_expectation) 313 314 315# Exceptions seenthe test passes.
REFACTOR: make it better
I remove the commented lines from
person.py4class Person: 5 6 def __init__( 7 self, first_name, last_name, 8 sex, year_of_birth=None, 9 is_citizen=True, 10 passed_test=False, 11 ): 12 self.first_name = first_name 13 self.last_name = last_name 14 self.year_of_birth = year_of_birth 15 self.sex = sex 16 self.is_citizen = is_citizen 17 self.passed_test = passed_test 18 19 def can_get_license(self): 20 return self.passed_test 21 22 def can_vote(self):I add a call to
can_get_licensefrom test_john, intest_person.py147 reality = john.say_hello() 148 assert reality == my_expectation 149 self.assertEqual(reality, my_expectation) 150 self.assertEqual(john.can_vote(), False) 151 self.assertEqual(john.can_get_license(), False) 152 153 def test_mary(self):the test passes because a method uses the default value for a parameter when it is called without the parameter.
I add a call to
can_get_licensefrom test_jane, intest_person.py99 reality = jane.say_hello() 100 assert reality == my_expectation 101 self.assertEqual(reality, my_expectation) 102 self.assertEqual(jane.can_vote(), True) 103 self.assertEqual(jane.can_get_license(), True) 104 105 def test_john(self):the terminal is my friend, and shows AssertionError
AssertionError: False != Truebecause a method uses the default value for a parameter when it is called without the parameter.
I add
passed_testto the call to the Person class forjane92 jane = src.person.Person( 93 first_name=first_name, 94 last_name=last_name, 95 sex=sex, 96 year_of_birth=year_of_birth, 97 passed_test=True, 98 ) 99 100 reality = jane.say_hello() 101 assert reality == my_expectation 102 self.assertEqual(reality, my_expectation) 103 self.assertEqual(jane.can_vote(), True) 104 self.assertEqual(jane.can_get_license(), True) 105 106 def test_john(self):the test passes.
I add a git commit message in the other terminal
git commit -am \ 'add passed_test attribute'
john and joe do not need to pass a value for the passed_test parameter because a method uses the default value for a parameter when it is called without the parameter.
add condition to can_get_license
I want the can_get_license method to use two conditions to make a decision
did the person pass the test?
is the person
18or older?
RED: make it fail
I go back to the terminal where the tests are running
I add an assertion to test_underage_citizen for a person who is younger than 18 and passed the test
202 def test_underage_citizen(self): 203 person = src.person.Person( 204 first_name='first_name', 205 last_name='last_name', 206 sex='M', 207 year_of_birth=datetime.date.today().year-17, 208 is_citizen=True, 209 passed_test=True, 210 ) 211 self.assertEqual(person.can_vote(), False) 212 self.assertEqual( 213 person.can_get_license(), False 214 ) 215 216 def test_when_year_of_birth_is_not_an_integer(self):the terminal is my friend, and shows AssertionError
AssertionError: True != Falsebecause
can_get_licensecurrently returns the value ofpassed_test. It does not care about the age of the person.
GREEN: make it pass
I add an if statement with a call to the calculate_age function from the can_get_license method in person.py
19 def can_get_license(self):
20 age = calculate_age(self.year_of_birth)
21 if age < 18:
22 return False
23 return self.passed_test
24
25 def can_vote(self):
the test passes because this happens when if age < 18: runs, Python checks if age which is the result of calculate_age(self.year_of_birth) is less than 18
If
ageis greater than or equal to18, it leaves the if statement and continues to run the rest of the method -return self.passed_test, which returnsFalse as the output, if the person failed the test
self.passed_test = False age >= 18 person.can_get_license └──def can_get_license(self): ├── if age < 18: │ return False └── return self.passed_testTrue as the output, if the person passed the test
self.passed_test = True age >= 18 person.can_get_license └──def can_get_license(self): ├── if age < 18: │ return False └── return self.passed_test
then leaves the function since the return statement is the last thing to run in a function.
If
ageis less than18, it goes to the next line -return False, which returns False as the output, then leaves the function since the return statement is the last thing to run in a function.self.passed_test = False age < 18 person.can_get_license └──def can_get_license(self): └── if age < 18: └── return False return self.passed_testself.passed_test = True age < 18 person.can_get_license └──def can_get_license(self): └── if age < 18: └── return False return self.passed_testI add a git commit message in the other terminal
git commit -am \ 'add condition to can_get_license'
extract age instance attribute
Three of the methods of the Person class call the calculate_age function.
RED: make it fail
I add an instance attribute to the __init__ method so that the age is calculated once when an instance is made, not every time one of the methods is called.
4class Person:
5
6 def __init__(
7 self, first_name, last_name,
8 sex, year_of_birth=None,
9 is_citizen=True,
10 passed_test=False,
11 ):
12 self.first_name = first_name
13 self.last_name = last_name
14 self.year_of_birth = year_of_birth
15 self.sex = sex
16 self.is_citizen = is_citizen
17 self.passed_test = passed_test
18 self.age = calculate_age(year_of_birth)
19
20 def can_get_license(self):
the terminal is my friend, and shows AssertionError
FAILED ...::TestPerson::test_dir_person_instance -
AssertionError: assert ['__class__',...'__eq__', ...]
== ['__class__',...'__eq__', ...]
FAILED ...::TestPerson::
test_when_year_of_birth_is_not_an_integer -
AssertionError
test_dir_person_instance fails because I just added a new class attribute.
test_when_year_of_birth_is_not_an_integer now fails when an instance of the Person class is made with a
year_of_birththat is not an integer.
how to skip a test
I can use `unittest.skip decorator`_ to skip a test. The problem with this solution is that I no longer know if the program does the thing the skipped test was written for.
GREEN: make it pass
I add the `unittest.skip decorator`_ to test_when_year_of_birth_is_not_an_integer with a note that it will always fail since it uses a year of birth that is not an integer, in
test_person.py216 @unittest.skip('will always fail') 217 def test_when_year_of_birth_is_not_an_integer(self): 218 person = src.person.Person( 219 first_name='first_name', 220 last_name='last_name', 221 sex='M', 222 # year_of_birth=None, # fails 223 # year_of_birth=False, # fails 224 # year_of_birth=2026.0, # fails 225 # year_of_birth='2026', # fails 226 # year_of_birth=(2026,), # fails 227 ) 228 # person.say_hello() 229 # fails if year_of_birth is not an integer 230 231 def test_dir_person_class(self):the terminal is my friend, and shows AssertionError
================ short test summary info ================= FAILED ...::TestPerson::test_dir_person_instance - AssertionError: assert ['__class__',...'__eq__', ...] == ['__class__',...'__eq__', ...] ======== 1 failed, 6 passed, 1 skipped in S.TUs ==========I add
agetomy_expectationin test_dir_person_instance309 'age', 310 'can_get_license', 311 'can_vote', 312 'first_name', 313 'is_citizen', 314 'last_name', 315 'passed_test', 316 'say_hello', 317 'sex', 318 'year_of_birth', 319 ] 320 assert reality == my_expectation 321 self.assertEqual(reality, my_expectation) 322 323 324# Exceptions seenthe test passes.
REFACTOR: make it better
I use
self.agein the can_get_license method inperson.py20 def can_get_license(self): 21 # age = calculate_age(self.year_of_birth) 22 # if age < 18: 23 if self.age < 18: 24 return False 25 return self.passed_test 26 27 def can_vote(self):the tests are still green.
I use
self.agein the can_vote method27 def can_vote(self): 28 # age = calculate_age(self.year_of_birth) 29 # if age < 18: 30 if self.age < 18: 31 return False 32 return self.is_citizen 33 34 def say_hello(self):still green.
I use
self.agein the say_hello method34 def say_hello(self): 35 return ( 36 f'Hello, my name is {self.first_name}' 37 f' {self.last_name} and I am' 38 # f' {calculate_age(self.year_of_birth)}.' 39 f' {self.age}.' 40 ) 41 42 43def calculate_age(year_of_birth):green.
I remove the commented line from the say_hello method
34 def say_hello(self): 35 return ( 36 f'Hello, my name is {self.first_name}' 37 f' {self.last_name} and I am' 38 f' {self.age}.' 39 ) 40 41 42def calculate_age(year_of_birth):I add a git commit message in the other terminal
git commit -am \ 'extract age instance attribute'
extract check_age method
can_get_license and can_vote look the same, they both
return False if
self.ageis less than18return something else if
self.ageis NOT less than18
RED: make it fail
I add a method to the Person class that checks if the age is less than 18 and returns something else if it is not
27 def can_vote(self):
28 # age = calculate_age(self.year_of_birth)
29 # if age < 18:
30 if self.age < 18:
31 return False
32 return self.is_citizen
33
34 def check_age(age, response):
35 if age < 18:
36 return False
37 return response
38
39 def say_hello(self):
the terminal is my friend, and shows AssertionError for test_dir_person_class and test_dir_person_instance because I added a new method.
GREEN: make it pass
I add
check_agetomy_expectationin test_dir_person_class intest_person.py263 'can_get_license', 264 'can_vote', 265 'check_age', 266 'say_hello', 267 ] 268 assert reality == my_expectation 269 self.assertEqual(reality, my_expectation) 270 271 def test_dir_person_instance(self):I add
check_agetomy_expectationin test_dir_person_instance310 'age', 311 'can_get_license', 312 'can_vote', 313 'check_age', 314 'first_name', 315 'is_citizen', 316 'last_name', 317 'passed_test', 318 'say_hello', 319 'sex', 320 'year_of_birth', 321 ] 322 assert reality == my_expectation 323 self.assertEqual(reality, my_expectation) 324 325 326# Exceptions seen 327# AssertionError 328# NameError 329# TypeError 330# AttributeError 331# SyntaxErrorthe test passes.
REFACTOR: make it better
I call the check_age method from the can_vote method in
person.py27 def can_vote(self): 28 # age = calculate_age(self.year_of_birth) 29 # if age < 18: 30 # if self.age < 18: 31 # return False 32 # return self.is_citizen 33 return self.check_age( 34 self.age, self.is_citizen 35 ) 36 37 def check_age(age, response):the terminal is my friend, and shows TypeError
TypeError: Person.check_age() takes 2 positional arguments but 3 were givenbecause a method of an instance takes the instance of the class (
self) it belongs to as the first argument.I add the staticmethod decorator to the check_age method since it does not use things from the Person class, only what it receives as input
37 @staticmethod 38 def check_age(age, response): 39 if age < 18: 40 return False 41 return response 42 43 def say_hello(self):the test passes.
I remove the commented lines from the can_vote method
27 def can_vote(self): 28 return self.check_age( 29 self.age, self.is_citizen 30 ) 31 32 @staticmethod 33 def check_age(age, response):I call the check_age method from the can_get_license method
20 def can_get_license(self): 21 # age = calculate_age(self.year_of_birth) 22 # if age < 18: 23 # if self.age < 18: 24 # return False 25 # return self.passed_test 26 return self.check_age( 27 self.age, self.passed_test 28 ) 29 30 def can_vote(self):the tests are still green.
I remove the commented lines from the can_get_license method
20 def can_get_license(self): 21 return self.check_age( 22 self.age, self.passed_test 23 ) 24 25 def can_vote(self):I add a git commit message in the other terminal
git commit -am \ 'extract check_age method'
close the project
I close
test_person.pyandperson.pyI click in the terminal where the tests are running
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
personcd ..the terminal shows
...\pumping_pythonI am back in the
pumping_pythondirectory.
review
I can use if statements to write a program that makes decisions based on conditions.
My tests have problems:
The attribute tests - test_dir_person_class and test_dir_person_instance catch changes to the attributes and methods of the Person class and they are a problem to maintain. There has to be a better way.
I skipped test_when_year_of_birth_is_not_an_integer because it is always in a RED state since it causes an Exception. The only way to know that the code causes the Exception is to remove the `unittest.skip decorator`_. There has to be a better way
test_joe, test_jane, test_john and test_mary also still have the problem where they are the same three tests. 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.
Would you like to test handling Exceptions in tests? Would you like to test booleans (there are only two)?
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.