how to make a person with conditions
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.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 it helps document what is in the class and catches things immediately it changes.
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 a class 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 a class 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 older than 18?
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 class attribute
Three of the methods of the Person class call the calculate_age function.
RED: make it fail
I add a class 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 class 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 __init__ method 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 seenthe 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 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?
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.