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. Which means I want something in the person project to make decisions based on conditions
If a person is younger than
18, the person cannot get a license.If a person is younger than
18, the person cannot vote.If a person is
18or older and passes a test, the person can get a license.If a person is
18or older and the person is a citizen, the person can vote.
preview
I have these tests by the end of the chapter
65 def test_joe(self):
66 first_name = 'joe'
67 last_name = 'blow'
68 sex = 'M'
69 year_of_birth = 1996
70
71 self.assert_person_factory_works(
72 first_name=first_name,
73 last_name=last_name,
74 sex=sex,
75 year_of_birth=year_of_birth
76 )
77
78 self.assert_say_hello_works(
79 first_name=first_name,
80 last_name=last_name,
81 year_of_birth=year_of_birth,
82 )
83
84 self.assert_person_can_say_hello(
85 first_name=first_name,
86 last_name=last_name,
87 sex=sex,
88 year_of_birth=year_of_birth
89 )
90
91 joe = src.person.Person(
92 first_name=first_name,
93 last_name=last_name,
94 sex=sex,
95 year_of_birth=year_of_birth,
96 )
97 self.assertEqual(joe.can_vote(), True)
98 self.assertEqual(joe.can_get_license(), False)
100 def test_jane(self):
101 first_name = 'jane'
102 last_name = 'doe'
103 sex = 'F'
104 year_of_birth = 1991
105
106 self.assert_person_factory_works(
107 first_name=first_name,
108 last_name=last_name,
109 sex=sex,
110 year_of_birth=year_of_birth,
111 )
112
113 self.assert_say_hello_works(
114 first_name=first_name,
115 last_name=last_name,
116 year_of_birth=year_of_birth,
117 )
118
119 self.assert_person_can_say_hello(
120 first_name=first_name,
121 last_name=last_name,
122 sex=sex,
123 year_of_birth=year_of_birth,
124 )
125
126 jane = src.person.Person(
127 first_name=first_name,
128 last_name=last_name,
129 sex=sex,
130 year_of_birth=year_of_birth,
131 passed_test=True,
132 )
133 self.assertEqual(jane.can_vote(), True)
134 self.assertEqual(jane.can_get_license(), True)
136 def test_john(self):
137 first_name = 'john'
138 last_name = 'smith'
139 sex = 'M'
140 year_of_birth = 1980
141
142 self.assert_person_factory_works(
143 first_name=first_name,
144 last_name=last_name,
145 sex=sex,
146 year_of_birth=year_of_birth,
147 )
148
149 self.assert_say_hello_works(
150 first_name=first_name,
151 last_name=last_name,
152 year_of_birth=year_of_birth,
153 )
154
155 self.assert_person_can_say_hello(
156 first_name=first_name,
157 last_name=last_name,
158 sex=sex,
159 year_of_birth=year_of_birth,
160 )
161
162 john = src.person.Person(
163 first_name=first_name,
164 last_name=last_name,
165 sex=sex,
166 year_of_birth=year_of_birth,
167 is_citizen=False,
168 passed_test=False,
169 )
170 self.assertEqual(john.can_vote(), False)
171 self.assertEqual(john.can_get_license(), False)
173 def test_mary(self):
174 first_name = 'mary'
175 last_name = 'public'
176 sex = 'F'
177 year_of_birth = 2000
178
179 self.assert_person_factory_works(
180 first_name=first_name,
181 last_name=last_name,
182 sex=sex,
183 year_of_birth=year_of_birth,
184 )
185
186 self.assert_say_hello_works(
187 first_name=first_name,
188 last_name=last_name,
189 year_of_birth=year_of_birth,
190 )
191
192 self.assert_person_can_say_hello(
193 first_name=first_name,
194 last_name=last_name,
195 sex=sex,
196 year_of_birth=year_of_birth,
197 )
198
199 mary = src.person.Person(
200 first_name=first_name,
201 last_name=last_name,
202 sex=sex,
203 year_of_birth=year_of_birth,
204 is_citizen=False,
205 passed_test=True,
206 )
207 self.assertEqual(mary.can_vote(), False)
208 self.assertEqual(mary.can_get_license(), True)
210 def test_underage_citizen(self):
211 underage = src.person.Person(
212 first_name='first_name',
213 last_name='last_name',
214 sex='M',
215 year_of_birth=self.this_year-17,
216 is_citizen=True,
217 passed_test=True,
218 )
219 self.assertEqual(underage.can_vote(), False)
220 self.assertEqual(underage.can_get_license(), False)
222 @unittest.skip('fails because age > 120')
223 def test_when_person_is_older_than_120(self):
224 src.person.Person(
225 first_name='first_name',
226 last_name='last_name',
227 sex='M',
228 year_of_birth=self.this_year-121
229 )
231 @unittest.skip('fails because age < 0')
232 def test_when_year_of_birth_is_the_future(self):
233 src.person.Person(
234 first_name='first_name',
235 last_name='last_name',
236 sex='F',
237 year_of_birth=self.this_year+1,
238 )
240 @unittest.skip(
241 'fails because year_of_birth is not an integer'
242 )
243 def test_when_year_of_birth_is_not_an_integer(self):
244 src.person.Person(
245 first_name='first_name',
246 last_name='last_name',
247 sex='M',
248 # year_of_birth=None, # fails
249 # year_of_birth=2026.0, # fails
250 # year_of_birth='2026', # fails
251 # year_of_birth=(2026,), # fails
252 )
254 def test_dir_person_class(self):
255 self.assertEqual(
256 dir(src.person.Person),
257 [
258 '__class__', '__delattr__', '__dict__',
259 '__dir__', '__doc__', '__eq__',
260 '__firstlineno__', '__format__', '__ge__',
261 '__getattribute__', '__getstate__', '__gt__',
262 '__hash__', '__init__', '__init_subclass__',
263 '__le__', '__lt__', '__module__', '__ne__',
264 '__new__', '__reduce__', '__reduce_ex__',
265 '__repr__', '__setattr__', '__sizeof__',
266 '__static_attributes__', '__str__',
267 '__subclasshook__', '__weakref__',
268 'can_get_license', 'can_vote', 'check_age',
269 'say_hello',
270 ]
271 )
273 def test_dir_person_instance(self):
274 self.assertEqual(
275 dir(
276 src.person.Person(
277 first_name='first_name',
278 last_name='last_name',
279 sex='M',
280 year_of_birth=2026,
281 )
282 ),
283 [
284 '__class__', '__delattr__', '__dict__',
285 '__dir__', '__doc__', '__eq__',
286 '__firstlineno__', '__format__', '__ge__',
287 '__getattribute__', '__getstate__', '__gt__',
288 '__hash__', '__init__', '__init_subclass__',
289 '__le__', '__lt__', '__module__', '__ne__',
290 '__new__', '__reduce__', '__reduce_ex__',
291 '__repr__', '__setattr__', '__sizeof__',
292 '__static_attributes__', '__str__',
293 '__subclasshook__', '__weakref__',
294 'age', 'can_get_license', 'can_vote',
295 'check_age', 'first_name', 'is_citizen',
296 'last_name', 'passed_test', 'say_hello',
297 'sex', 'year_of_birth',
298 ]
299 )
300
301
302# Exceptions seen
303# AssertionError
304# NameError
305# TypeError
306# AttributeError
307# 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%] =================== 9 passed in P.QRs ====================
add can_vote method
RED: make it fail
I add a call to can_vote from test_joe
65 def test_joe(self):
66 first_name = 'joe'
67 last_name = 'blow'
68 ...
84 self.assert_person_can_say_hello(
85 first_name=first_name,
86 last_name=last_name,
87 sex=sex,
88 year_of_birth=year_of_birth
89 )
90
91 joe = src.person.Person(
92 first_name=first_name,
93 last_name=last_name,
94 sex=sex,
95 year_of_birth=year_of_birth,
96 )
97 self.assertEqual(joe.can_vote(), True)
98
99 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 add a method definition to the Person class in
src/person/__init__.py4class 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):the terminal is my friend, and shows TypeError
TypeError: Person.can_vote() takes 0 positional arguments but 1 was givenI add the staticmethod decorator
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 @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_class intests/test_person.py210 def test_dir_person_class(self): 211 self.assertEqual( 212 dir(src.person.Person), 213 [ 214 '__class__', '__delattr__', '__dict__', 215 ...222 '__static_attributes__', '__str__', 223 '__subclasshook__', '__weakref__', 224 'can_vote', 'say_hello', 225 ] 226 ) 227 228 def test_dir_person_instance(self):I add
can_voteto test_dir_person_instance228 def test_dir_person_instance(self): 229 self.assertEqual( 230 dir( 231 src.person.Person( 232 first_name='first_name', 233 last_name='last_name', 234 sex='M', 235 year_of_birth=2026, 236 ) 237 ), 238 [ 239 '__class__', '__delattr__', '__dict__', 240 ...248 '__subclasshook__', '__weakref__', 249 'can_vote', 'first_name', 'last_name', 250 'say_hello', 'sex', 'year_of_birth', 251 ] 252 ) 253 254 255# Exceptions seenthe test passes.
test_dir_person_class and test_dir_person_instance are good because they help document what is in the object and catch its changes immediately.
test_dir_person_class and test_dir_person_instance are a problem because object attributes can change from one Python version to another, 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_vote from test_mary
151 def test_mary(self): 152 first_name = 'mary' 153 last_name = 'public' 154 ...170 self.assert_person_can_say_hello( 171 first_name=first_name, 172 last_name=last_name, 173 sex=sex, 174 year_of_birth=year_of_birth, 175 ) 176 177 mary = src.person.Person( 178 first_name=first_name, 179 last_name=last_name, 180 sex=sex, 181 year_of_birth=year_of_birth 182 ) 183 self.assertEqual(mary.can_vote(), False) 184 185 def test_when_person_is_older_than_120(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 instance of the Person class formary177 mary = src.person.Person( 178 first_name=first_name, 179 last_name=last_name, 180 sex=sex, 181 year_of_birth=year_of_birth, 182 is_citizen=False, 183 ) 184 self.assertEqual(mary.can_vote(), False)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 of the Person class only takes five inputs (
self,first_name,last_name,sexandyear_of_birth) and it got called it withis_citizenwhich is not one of those names.I add
is_citizento the parentheses of the __init__ method, insrc/person/__init__.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=True, 10 ):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=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 @staticmethod 18 def can_vote():still AssertionError.
I use the object attribute in the can_vote method
17 @staticmethod 18 def can_vote(): 19 return self.is_citizen 20 return Truethe 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 parentheses17 # @staticmethod 18 # def can_vote(): 19 def can_vote(self): 20 return self.is_citizen 21 return Truethe terminal is my friend, and shows AssertionError for test_dir_person_instance because I added a new attribute (
is_citizen).I add
is_citizento the expectation of the assertion in test_dir_person_instance intests/test_person.py237 def test_dir_person_instance(self): 238 self.assertEqual( 239 dir( 240 src.person.Person( 241 first_name='first_name', 242 last_name='last_name', 243 sex='M', 244 year_of_birth=2026, 245 ) 246 ), 247 [ 248 '__class__', '__delattr__', '__dict__', 249 ...257 '__subclasshook__', '__weakref__', 258 'can_vote', 'first_name', 'is_citizen', 259 'last_name', 'say_hello', 'sex', 260 'year_of_birth', 261 ] 262 ) 263 264 265# Exceptions seenthe test passes.
I remove the commented lines and the second return statement from the can_vote method in
src/person/__init__.py6 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):
REFACTOR: make it better
I add a call to can_vote from test_john, in
tests/test_person.py125 def test_john(self): 126 first_name = 'john' 127 last_name = 'smith' 128 ...144 self.assert_person_can_say_hello( 145 first_name=first_name, 146 last_name=last_name, 147 sex=sex, 148 year_of_birth=year_of_birth, 149 ) 150 151 john = src.person.Person( 152 first_name=first_name, 153 last_name=last_name, 154 sex=sex, 155 year_of_birth=year_of_birth, 156 ) 157 self.assertEqual(john.can_vote(), False) 158 159 def test_mary(self):the terminal is my friend, and shows AssertionError
AssertionError: True != FalseI add
is_citizento the instance of the Person class forjohn151 john = src.person.Person( 152 first_name=first_name, 153 last_name=last_name, 154 sex=sex, 155 year_of_birth=year_of_birth, 156 is_citizen=False, 157 ) 158 self.assertEqual(john.can_vote(), False)the test passes.
I add a call to can_vote from test_jane
99 def test_jane(self): 100 first_name = 'jane' 101 last_name = 'doe' 102 ...118 self.assert_person_can_say_hello( 119 first_name=first_name, 120 last_name=last_name, 121 sex=sex, 122 year_of_birth=year_of_birth, 123 ) 124 125 jane = src.person.Person( 126 first_name=first_name, 127 last_name=last_name, 128 sex=sex, 129 year_of_birth=year_of_birth, 130 ) 131 self.assertEqual(jane.can_vote(), False) 132 133 def test_john(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 and the default value for
is_citizenis True.I change the expectation of the assertion in test_jane
125 jane = src.person.Person( 126 first_name=first_name, 127 last_name=last_name, 128 sex=sex, 129 year_of_birth=year_of_birth, 130 ) 131 # self.assertEqual(jane.can_vote(), False) 132 self.assertEqual(jane.can_vote(), True) 133 134 def test_john(self):the test passes.
I remove the commented line from test_jane
125 jane = src.person.Person( 126 first_name=first_name, 127 last_name=last_name, 128 sex=sex, 129 year_of_birth=year_of_birth, 130 ) 131 self.assertEqual(jane.can_vote(), True)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.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
18201 self.assertEqual(mary.can_vote(), False) 202 203 def test_underage_citizen(self): 204 underage = src.person.Person( 205 first_name='first_name', 206 last_name='last_name', 207 sex='M', 208 year_of_birth=self.this_year-17, 209 is_citizen=True, 210 ) 211 self.assertEqual(underage.can_vote(), False) 212 213 def test_when_person_is_older_than_120(self):the terminal is my friend, and shows AssertionError
AssertionError: True != Falsebecause can_vote returns the value of
is_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
src/person/__init__.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 Python checks if
age(which is the result ofcalculate_age(self.year_of_birth)) is less than18, whenif age < 18:runsIf
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() -> False └── class Person: └── def can_vote(self): └── if age < 18: └── return False return self.is_citizenself.is_citizen = True age < 18 person.can_vote() -> False └── class Person: └── def can_vote(self): └── if age < 18: └── return False return self.is_citizenIf
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() -> False └── class Person: └── 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() -> True └── class Person: └── 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.
I 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 go back to the terminal where the tests are running
I add a call to can_get_license from test_mary in
tests/test_person.py194 mary = src.person.Person( 195 first_name=first_name, 196 last_name=last_name, 197 sex=sex, 198 year_of_birth=year_of_birth, 199 is_citizen=False, 200 ) 201 self.assertEqual(mary.can_vote(), False) 202 self.assertEqual(mary.can_get_license(), True) 203 204 def test_underage_citizen(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 for can_get_license to the Person class in
src/person/__init__.py6 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
15 self.is_citizen = is_citizen 16 17 @staticmethod 18 def can_get_license(): 19 return Truethe 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 intests/test_person.py247 def test_dir_person_class(self): 248 self.assertEqual( 249 dir(src.person.Person), 250 [ 251 '__class__', '__delattr__', '__dict__', 252 ...260 '__subclasshook__', '__weakref__', 261 'can_get_license', 'can_vote', 'say_hello' 262 ] 263 ) 264 265 def test_dir_person_instance(self):I add
can_get_licenseto test_dir_person_instance265 def test_dir_person_instance(self): 266 self.assertEqual( 267 dir( 268 src.person.Person( 269 first_name='first_name', 270 last_name='last_name', 271 sex='M', 272 year_of_birth=2026, 273 ) 274 ), 275 [ 276 '__class__', '__delattr__', '__dict__', 277 ...285 '__subclasshook__', '__weakref__', 286 'can_get_license', 'can_vote', 'first_name', 287 'is_citizen', 'last_name', 'say_hello', 288 'sex', 'year_of_birth', 289 ] 290 ) 291 292 293# 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 go back to the terminal where the tests are running
I add a call to can_get_license from test_john
159 john = src.person.Person( 160 first_name=first_name, 161 last_name=last_name, 162 sex=sex, 163 year_of_birth=year_of_birth, 164 is_citizen=False, 165 ) 166 self.assertEqual(john.can_vote(), False) 167 self.assertEqual(john.can_get_license(), False) 168 169 def test_mary(self):the terminal is my friend, and shows AssertionError
AssertionError: True != False
GREEN: make it pass
I add
passed_testto the instance of the Person class forjohn159 john = src.person.Person( 160 first_name=first_name, 161 last_name=last_name, 162 sex=sex, 163 year_of_birth=year_of_birth, 164 is_citizen=False, 165 passed_test=False, 166 ) 167 self.assertEqual(john.can_vote(), False) 168 self.assertEqual(john.can_get_license(), False)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 allows six inputs (
self,first_name,last_name,sex,year_of_birthandis_citizen) and it got called withpassed_testwhich is not one of those names.I add
passed_testto the parentheses of the __init__ method, insrc/person/__init__.py4class Person: 5 6 def __init__( 7 self, first_name, last_name, 8 sex, year_of_birth=None, 9 is_citizen=True, passed_test, 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
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, passed_test=False, 10 ):the terminal goes back to the AssertionError.
I use
self.passed_testin the can_get_license method17 @staticmethod 18 def can_get_license(): 19 return self.passed_test 20 return Truethe 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 parentheses17 # @staticmethod 18 # def can_get_license(): 19 def can_get_license(self): 20 return self.passed_test 21 return Truethe terminal is my friend, and shows AttributeError
AttributeError: 'Person' object has no attribute 'passed_test'I add an instance attribute for
passed_testso I can use it in the can_get_license method6 def __init__( 7 self, first_name, last_name, 8 sex, year_of_birth=None, 9 is_citizen=True, passed_test=False, 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 self.passed_test = passed_test 17 18 @staticmethod 19 def can_get_license():the terminal shows AssertionError for test_mary
AssertionError: False != TrueI add
passed_testto the instance of the Person class formary196 mary = src.person.Person( 197 first_name=first_name, 198 last_name=last_name, 199 sex=sex, 200 year_of_birth=year_of_birth, 201 is_citizen=False, 202 passed_test=True, 203 ) 204 self.assertEqual(mary.can_vote(), False) 205 self.assertEqual(mary.can_get_license(), True)the terminal shows AssertionError for test_dir_person_instance because I added a new attribute (
passed_test).I add
passed_testto the expectation of the assertion in test_dir_person_instance intests/test_person.py268 def test_dir_person_instance(self): 269 self.assertEqual( 270 dir( 271 src.person.Person( 272 first_name='first_name', 273 last_name='last_name', 274 sex='M', 275 year_of_birth=2026, 276 ) 277 ), 278 [ 279 '__class__', '__delattr__', '__dict__', 280 ...289 'can_get_license', 'can_vote', 'first_name', 290 'is_citizen', 'last_name', 'passed_test', 291 'say_hello', 'sex', 'year_of_birth', 292 ] 293 ) 294 295 296# Exceptions seenthe test passes.
REFACTOR: make it better
I remove the commented lines and the second return statement from the can_get_license method in
src/person/__init__.py15 self.is_citizen = is_citizen 16 self.passed_test = passed_test 17 18 def can_get_license(self): 19 return self.passed_test 20 21 def can_vote(self):I add a call to can_get_license from test_jane, in
tests/test_person.py125 jane = src.person.Person( 126 first_name=first_name, 127 last_name=last_name, 128 sex=sex, 129 year_of_birth=year_of_birth, 130 ) 131 self.assertEqual(jane.can_vote(), True) 132 self.assertEqual(jane.can_get_license(), True) 133 134 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 instance of the Person class forjane125 jane = src.person.Person( 126 first_name=first_name, 127 last_name=last_name, 128 sex=sex, 129 year_of_birth=year_of_birth, 130 passed_test=True, 131 ) 132 self.assertEqual(jane.can_vote(), True) 133 self.assertEqual(jane.can_get_license(), True)the test passes.
I add a call to can_get_license from test_joe
91 joe = src.person.Person( 92 first_name=first_name, 93 last_name=last_name, 94 sex=sex, 95 year_of_birth=year_of_birth, 96 ) 97 self.assertEqual(joe.can_vote(), True) 98 self.assertEqual(joe.can_get_license(), True) 99 100 def test_jane(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 change the expectation of the assertion for the can_get_license method in test_joe
97 self.assertEqual(joe.can_vote(), True) 98 # self.assertEqual(joe.can_get_license(), True) 99 self.assertEqual(joe.can_get_license(), False) 100 101 def test_jane(self):the test passes.
I remove the commented line from test_joe
91 joe = src.person.Person( 92 first_name=first_name, 93 last_name=last_name, 94 sex=sex, 95 year_of_birth=year_of_birth, 96 ) 97 self.assertEqual(joe.can_vote(), True) 98 self.assertEqual(joe.can_get_license(), False)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
210 def test_underage_citizen(self): 211 underage = src.person.Person( 212 first_name='first_name', 213 last_name='last_name', 214 sex='M', 215 year_of_birth=self.this_year-17, 216 is_citizen=True, 217 passed_test=True, 218 ) 219 self.assertEqual(underage.can_vote(), False) 220 self.assertEqual(underage.can_get_license(), False) 221 222 def test_when_person_is_older_than_120(self):the terminal is my friend, and shows AssertionError
AssertionError: True != Falsebecause can_get_license currently returns the value of
passed_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 src/person/__init__.py
18 def can_get_license(self):
19 age = calculate_age(self.year_of_birth)
20 if age < 18:
21 return False
22 return self.passed_test
23
24 def can_vote(self):
the test passes because Python checks if age (the result of calculate_age(self.year_of_birth)) is less than 18, when if age < 18: runs
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() -> False └── class Person: └── def can_get_license(self): └── if age < 18: └── return False return self.passed_testself.passed_test = True age < 18 person.can_get_license() -> False └── class Person: └── def can_get_license(self): └── if age < 18: └── return False return self.passed_testIf
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() -> False └── class Person: └── 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() -> True └── class Person: └── 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.
I add a git commit message in the other terminal
git commit -am \ 'add condition to can_get_license'
extract age instance attribute
The can_get_license, can_vote and say_hello methods of the Person class all call the calculate_age function.
I can make an attribute to remove the repetition of the calls to the calculate_age function.
RED: make it fail
I go back to the terminal where the tests are running
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.
4 class Person: 5 6 def __init__( 7 self, first_name, last_name, 8 sex, year_of_birth=None, 9 is_citizen=True, passed_test=False, 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 self.passed_test = passed_test 17 self.age = calculate_age(year_of_birth) 18 19 def can_get_license(self):the terminal is my friend, and shows AssertionError
FAILED ...test_dir_person_instance - AssertionError: Lists differ: ['__c[396 chars]_', 'age', 'can_ge... FAILED ...test_when_person_is_older_than_120 - AssertionError FAILED ...test_when_year_of_birth_is_not_an_integer - AssertionError FAILED ...test_when_year_of_birth_is_the_future - AssertionErrortest_when_year_of_birth_is_the_future fails because the age is smaller than
0.test_when_year_of_birth_is_not_an_integer fails because an instance of the Person class is made with a
year_of_birth(None) that is not an integer.test_when_person_is_older_than_120 fails because the age is bigger than
120.test_dir_person_instance fails because I just added a new object attribute.
how to skip a test
I can use unittest.skip decorator to skip a test. The problem with skipping tests is that I will no longer know if the program does the thing the skipped tests were written for.
GREEN: make it pass
I add the unittest.skip decorator to test_when_person_is_older_than_120 with a note that it fails because the age is bigger than
120, intests/test_person.py219 self.assertEqual(underage.can_vote(), False) 220 self.assertEqual(underage.can_get_license(), False) 221 222 @unittest.skip('fails because age > 120') 223 def test_when_person_is_older_than_120(self): 224 src.person.Person( 225 first_name='first_name', 226 last_name='last_name', 227 sex='M', 228 year_of_birth=self.this_year-121 229 ) 230 # ).say_hello() fails 231 # because age > 120 232 233 def test_when_year_of_birth_is_the_future(self):I remove the comments from test_when_person_is_older_than_120 because they are a repetition of the message in the unittest.skip decorator
222 @unittest.skip('fails because age > 120') 223 def test_when_person_is_older_than_120(self): 224 src.person.Person( 225 first_name='first_name', 226 last_name='last_name', 227 sex='M', 228 year_of_birth=self.this_year-121 229 ) 230 231 def test_when_year_of_birth_is_the_future(self):I add the unittest.skip decorator to test_when_year_of_birth_is_the_future with a message that it will always fail since it uses a year of birth that is in the future
231 @unittest.skip('fails because age < 0') 232 def test_when_year_of_birth_is_the_future(self): 233 src.person.Person( 234 first_name='first_name', 235 last_name='last_name', 236 sex='F', 237 year_of_birth=self.this_year+1, 238 ) 239 # ).say_hello() fails 240 # because age < 0I remove the comments from test_when_year_of_birth_is_the_future
231 @unittest.skip('fails because age < 0') 232 def test_when_year_of_birth_is_the_future(self): 233 src.person.Person( 234 first_name='first_name', 235 last_name='last_name', 236 sex='F', 237 year_of_birth=self.this_year+1, 238 ) 239 240 def test_when_year_of_birth_is_not_an_integer(self):I add the unittest.skip decorator to test_when_year_of_birth_is_not_an_integer with a message
240 @unittest.skip( 241 'fails because year_of_birth is not an integer' 242 ) 243 def test_when_year_of_birth_is_not_an_integer(self): 244 src.person.Person( 245 first_name='first_name', 246 last_name='last_name', 247 sex='M', 248 # year_of_birth=None, # fails 249 # year_of_birth=2026.0, # fails 250 # year_of_birth='2026', # fails 251 # year_of_birth=(2026,), # fails 252 ) 253 # ).say_hello() fails 254 # because year_of_birth is not an integer 255 256 def test_dir_person_class(self):I remove the repeated message from test_when_year_of_birth_is_not_an_integer
240 @unittest.skip( 241 'fails because year_of_birth is not an integer' 242 ) 243 def test_when_year_of_birth_is_not_an_integer(self): 244 src.person.Person( 245 first_name='first_name', 246 last_name='last_name', 247 sex='M', 248 # year_of_birth=None, # fails 249 # year_of_birth=2026.0, # fails 250 # year_of_birth='2026', # fails 251 # year_of_birth=(2026,), # fails 252 ) 253 254 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, 3 skipped in S.TUs ==========I add
ageto the expectation of the assertion in test_dir_person_instance272 def test_dir_person_instance(self): 273 self.assertEqual( 274 dir( 275 src.person.Person( 276 first_name='first_name', 277 last_name='last_name', 278 sex='M', 279 year_of_birth=2026, 280 ) 281 ), 282 [ 283 '__class__', '__delattr__', '__dict__', 284 ...292 '__subclasshook__', '__weakref__', 293 'age', 'can_get_license', 'can_vote', 294 'first_name', 'is_citizen', 'last_name', 295 'passed_test', 'say_hello', 'sex', 296 'year_of_birth', 297 ] 298 ) 299 300 301# Exceptions seenthe test passes.
REFACTOR: make it better
I use
self.agein the can_get_license method insrc/person/__init__.py19 def can_get_license(self): 20 # age = calculate_age(self.year_of_birth) 21 # if age < 18: 22 if self.age < 18: 23 return False 24 return self.passed_test 25 26 def can_vote(self):the tests are still green.
I use
self.agein the can_vote method26 def can_vote(self): 27 # age = calculate_age(self.year_of_birth) 28 # if age < 18: 29 if self.age < 18: 30 return False 31 return self.is_citizen 32 33 def say_hello(self):still green.
I use
self.agein the say_hello method33 def say_hello(self): 34 return ( 35 f'Hello, my name is {self.first_name}' 36 f' {self.last_name} and I am' 37 # f' {calculate_age(self.year_of_birth)}.' 38 f' {self.age}.' 39 ) 40 41 42def calculate_age(year_of_birth):green.
I remove the commented line from the say_hello method
33 def say_hello(self): 34 return ( 35 f'Hello, my name is {self.first_name}' 36 f' {self.last_name} and I am {self.age}.' 37 ) 38 39 40def 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
if self.age < 18:
return False
return something_else
I can make a method that removes the repetition.
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
16 self.passed_test = passed_test
17 self.age = calculate_age(year_of_birth)
18
19 def check_age(status):
20 if self.age < 18:
21 return False
22 return status
23
24 def can_get_license(self):
the terminal is my friend, and shows AssertionError
FAILED ...test_dir_person_class - AssertionError:
Lists differ: ['__c[393 chars]ef__', 'can_get_li...
FAILED ...test_dir_person_instance - AssertionError:
Lists differ: ['__c[434 chars]e', 'check_age', '...
because I added a new method.
GREEN: make it pass
I add
check_ageto the expectation of the assertion in test_dir_person_instance272 def test_dir_person_instance(self): 273 self.assertEqual( 274 dir( 275 src.person.Person( 276 first_name='first_name', 277 last_name='last_name', 278 sex='M', 279 year_of_birth=2026, 280 ) 281 ), 282 [ 283 '__class__', '__delattr__', '__dict__', 284 ...292 '__subclasshook__', '__weakref__', 293 'age', 'can_get_license', 'can_vote', 294 'check_age', 'first_name', 'is_citizen', 295 'last_name', 'passed_test', 'say_hello', 296 'sex', 'year_of_birth', 297 ] 298 ) 299 300 301# Exceptions seen 302# AssertionError 303# NameError 304# TypeError 305# AttributeError 306# SyntaxErrorI add
check_ageto the expectation of the assertion in test_dir_person_class intests/test_person.py254 def test_dir_person_class(self): 255 self.assertEqual( 256 dir(src.person.Person), 257 [ 258 '__class__', '__delattr__', '__dict__', 259 ...267 '__subclasshook__', '__weakref__', 268 'can_get_license', 'can_vote', 'check_age', 269 'say_hello' 270 ] 271 ) 272 273 def test_dir_person_instance(self):the test passes.
REFACTOR: make it better
I call the check_age method from the can_get_license method in
src/person/__init__.py24 def can_get_license(self): 25 return self.check_age(self.passed_test) 26 # age = calculate_age(self.year_of_birth) 27 # if age < 18: 28 if self.age < 18: 29 return False 30 return self.passed_test 31 32 def can_vote(self):the terminal is my friend, and shows TypeError
TypeError: Person.check_age() takes 1 positional arguments but 2 were givenbecause a method of an instance takes the instance of the class (
self) it belongs to as the first argument.I add
selfto the parentheses of the check_age method16 self.passed_test = passed_test 17 self.age = calculate_age(year_of_birth) 18 19 def check_age(self, status): 20 if self.age < 18: 21 return False 22 return status 23 24 def can_get_license(self):the test passes.
I remove the other statements from the can_get_license method
24 def can_get_license(self): 25 return self.check_age(self.passed_test) 26 27 def can_vote(self):I call the check_age method from the can_vote method
27 def can_vote(self): 28 return self.check_age(self.is_citizen) 29 # age = calculate_age(self.year_of_birth) 30 # if age < 18: 31 if self.age < 18: 32 return False 33 return self.is_citizen 34 35 def say_hello(self):the tests are still green.
I remove the other statements from the can_vote method
27 def can_vote(self): 28 return self.check_age(self.is_citizen) 29 30 def say_hello(self):I add a git commit message in the other terminal
git commit -am \ 'extract check_age method'
close the project
I close
tests/test_person.pyandsrc/person/__init__.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_person_is_older_than_120, test_when_year_of_birth_is_the_future and test_when_year_of_birth_is_not_an_integer because they are always in a RED state since they cause 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.