test person with datetime
The person project has a problem with the calculation of the ages. It only shows the right age if the program is run in 2026, because the year is hardcoded. If I run it in a different year or change the year on my computer, the ages will be wrong and the tests for say_hello will fail.
I want the calculation to always be right, which means the program should always know the correct year.
I can use the datetime module from The Python Standard Library. You can think of it as a toolbox with different tools I can use to do things with dates and times. I can also use assertions to make sure I get the right year of birth for the calculations.
preview
I add the following code by the end of the chapter
1import datetime
2import src.person
3import unittest
4
5
6class TestPerson(unittest.TestCase):
7
8 this_year = datetime.date.today().year
9
10 def assert_person_factory_works(
11 self, first_name, last_name,
12 sex, year_of_birth
13 ):
27 def calculate_age(self, year_of_birth):
28 return self.this_year - year_of_birth
30 def assert_say_hello_works(
31 self, first_name, last_name,
32 year_of_birth,
33 ):
34 self.assertEqual(
35 src.person.say_hello(
36 first_name=first_name,
37 last_name=last_name,
38 year_of_birth=year_of_birth
39 ),
40 (
41 f'Hello, my name is {first_name}'
42 f' {last_name} and I am'
43 f' {self.calculate_age(year_of_birth)}.'
44 )
45 )
47 def assert_person_can_say_hello(
48 self,first_name, last_name,
49 sex, year_of_birth,
50 ):
51 self.assertEqual(
52 src.person.Person(
53 first_name=first_name,
54 last_name=last_name,
55 sex=sex,
56 year_of_birth=year_of_birth,
57 ).say_hello(),
58 (
59 f'Hello, my name is {first_name}'
60 f' {last_name} and I am'
61 f' {self.calculate_age(year_of_birth)}.'
62 )
63 )
117 def test_john(self):
118 first_name = 'john'
119 last_name = 'smith'
120 sex = 'M'
121 year_of_birth = 1980
122
123 self.assert_person_factory_works(
124 first_name=first_name,
125 last_name=last_name,
126 sex=sex,
127 year_of_birth=year_of_birth,
128 )
129
130 self.assert_say_hello_works(
131 first_name=first_name,
132 last_name=last_name,
133 year_of_birth=year_of_birth,
134 )
135
136 self.assert_person_can_say_hello(
137 first_name=first_name,
138 last_name=last_name,
139 sex=sex,
140 year_of_birth=year_of_birth,
141 )
169 def test_when_person_is_older_than_120(self):
170 src.person.Person(
171 first_name='first_name',
172 last_name='last_name',
173 sex='M',
174 year_of_birth=self.this_year-121
175 )
176 # ).say_hello() fails
177 # because age > 120
179 def test_when_year_of_birth_is_the_future(self):
180 src.person.Person(
181 first_name='first_name',
182 last_name='last_name',
183 sex='F',
184 year_of_birth=self.this_year+1,
185 )
186 # ).say_hello() fails
187 # because age < 0
189 def test_when_year_of_birth_is_not_an_integer(self):
190 src.person.Person(
191 first_name='first_name',
192 last_name='last_name',
193 sex='M',
194 # year_of_birth=None, # fails
195 # year_of_birth=2026.0, # fails
196 # year_of_birth='2026', # fails
197 # year_of_birth=(2026,), # fails
198 )
199 # ).say_hello() fails
200 # because year_of_birth is not an integer
201
202 def test_dir_person_class(self):
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%] =================== 6 passed in M.NOs ====================
test_dir_datetime
I want to see what comes with the datetime module.
RED: make it fail
I add test_dir_datetime to
tests/test_person.py180 def test_dir_person_instance(self): 181 self.assertEqual( 182 dir( 183 src.person.Person( 184 first_name='first_name', 185 last_name='last_name', 186 sex='M', 187 year_of_birth=2026, 188 ) 189 ), 190 [ 191 '__class__', '__delattr__', '__dict__',201 'last_name', 'say_hello', 'sex', 'year_of_birth', 202 ] 203 ) 204 205 def test_dir_datetime(self): 206 self.assertEqual( 207 dir(datetime), 208 [] 209 ) 210 211 212# Exceptions seenthe terminal is my friend, and shows NameError
NameError: name 'datetime' is not defined. Did you forget to import 'datetime'?
GREEN: make it pass
I add an import statement for datetime
1import datetime 2import src.person 3import unittest 4 5 6class TestPerson(unittest.TestCase):the terminal is my friend, and shows AssertionError
AssertionError: Lists differ: [ 'MAXYEAR', 'MINYEAR', 'UTC', '__all__', '[179 chars]nfo' ] != []it also shows the entire difference between the lists
I copy (ctrl/command+c) the values from the terminal, paste (ctrl/command+v) them as the expectation of the assertion and remove the extra characters, in test_dir_datetime
Caution
Your list of attributes and methods may be different depending on your Python version
206 def test_dir_datetime(self): 207 self.assertEqual( 208 dir(datetime), 209 [ 210 'MAXYEAR', 211 'MINYEAR', 212 'UTC', 213 '__all__', 214 '__builtins__', 215 '__cached__', 216 '__doc__', 217 '__file__', 218 '__loader__', 219 '__name__', 220 '__package__', 221 '__spec__', 222 'date', 223 'datetime', 224 'datetime_CAPI', 225 'time', 226 'timedelta', 227 'timezone', 228 'tzinfo' 229 ] 230 ) 231 232 233# Exceptions seenthe test passes because when
import datetimeruns, Python brings in an object (everything in Python is an object) for the datetime module from The Python Standard Library so I can use it intests/test_person.pyasdatetime.This means that there is a file or folder on the computer named
datetimethat got added when I installed Python.
test_dir_datetime_date
A few names stand out in the list of attributes and methods of datetime
date- I assume this handles datestime- I assume this handles timedatetime- I assume a combination of date and time
What I want is something that will give me the current year.
RED: make it fail
I add test_dir_datetime_date to tests/test_person.py
206 def test_dir_datetime(self):
207 self.assertEqual(
208 dir(datetime),
209 [
210 'MAXYEAR',
228 'tzinfo'
229 ]
230 )
231
232 def test_dir_datetime_date(self):
233 self.assertEqual(
234 dir(datetime.date),
235 []
236 )
237
238
239 # Exceptions seen
the terminal is my friend, and shows AssertionError
AssertionError:
Lists differ: [
'__add__', '__class__', '__delattr__',
'_[585 chars]ear'
] != []
with a message about how to see the entire difference
Diff is 787 characters long.
Set self.maxDiff to None to see it.
GREEN: make it pass
I set self.maxDiff to None
232 def test_dir_datetime_date(self): 233 self.maxDiff = None 234 self.assertEqual( 235 dir(datetime.date), 236 [] 237 ) 238 239 240# Exceptions seenI copy (ctrl/command+c) the values from the terminal, paste (ctrl/command+v) them as
my_expectationand remove the extra characters232 def test_dir_datetime_date(self): 233 self.maxDiff = None 234 self.assertEqual( 235 dir(datetime.date), 236 [ 237 '__add__', '__class__', '__delattr__', 238 '__dir__', '__doc__', '__eq__', 239 '__format__', '__ge__', '__getattribute__', 240 '__getstate__', '__gt__', '__hash__', 241 '__init__', '__init_subclass__', '__le__', 242 '__lt__', '__ne__', '__new__', '__radd__', 243 '__reduce__', '__reduce_ex__', 244 '__replace__', '__repr__', '__rsub__', 245 '__setattr__', '__sizeof__', '__str__', 246 '__sub__', '__subclasshook__', 'ctime', 247 'day', 'fromisocalendar', 'fromisoformat', 248 'fromordinal', 'fromtimestamp', 249 'isocalendar', 'isoformat', 'isoweekday', 250 'max', 'min', 'month', 'replace', 251 'resolution', 'strftime', 'strptime', 252 'timetuple', 'today', 'toordinal', 253 'weekday', 'year' 254 ] 255 ) 256 257 258# Exceptions seenthe test passes.
test_dir_datetime_date_year
I see year in the list of attributes and methods of datetime.date.
RED: make it fail
I add a test for the year attribute of the date attribute of the datetime module in tests/test_person.py
253 'weekday', 'year'
254 ]
255 )
256
257 def test_dir_datetime_date_year(self):
258 self.assertEqual(
259 dir(datetime.date.year),
260 []
261 )
262
263
264# Exceptions seen
the terminal is my friend, and shows AssertionError with only attributes that start and end with double underscore (__)
GREEN: make it pass
I change the assertion to see what the
datetime.date.yearobject is257 def test_dir_datetime_date_year(self): 258 self.assertEqual( 259 # dir(datetime.date.year), 260 datetime.date.year, 261 [] 262 ) 263 264 265# Exceptions seenthe terminal is my friend, and shows AssertionError
AssertionError: <attribute 'year' of 'datetime.date' objects> != []I try making an instance of the datetime.date object
257 def test_dir_datetime_date_year(self): 258 self.assertEqual( 259 # dir(datetime.date.year), 260 # datetime.date.year, 261 datetime.date().year, 262 [] 263 ) 264 265 266# Exceptions seenthe terminal is my friend, and shows TypeError
TypeError: function missing required argument 'year' (pos 1)It is a function that has required input. I want something that automatically knows the date and gives me the year.
test_dir_datetime_date_today
I also saw today in the list of attributes and methods of datetime.date.
RED: make it fail
I change test_dir_datetime_date_year to a test for the today attribute of the date attribute of the datetime module in tests/test_person.py
253 'weekday', 'year'
254 ]
255 )
256
257 # def test_dir_datetime_date_year(self):
258 def test_dir_datetime_date_today(self):
259 self.assertEqual(
260 # dir(datetime.date.year),
261 # datetime.date.year,
262 # datetime.date().year,
263 datetime.date.today,
264 []
265 )
266
267
268# Exceptions seen
the terminal is my friend, and shows AssertionError
AssertionError:
<built-in method today
of type object at 0xffff0fab2345>
!= []
GREEN: make it pass
I call the
datetime.date.todaymethod to see what it returns257 # def test_dir_datetime_date_year(self): 258 def test_dir_datetime_date_today(self): 259 self.assertEqual( 260 # dir(datetime.date.year), 261 # datetime.date.year, 262 # datetime.date().year, 263 # datetime.date.today, 264 datetime.date.today(), 265 [] 266 ) 267 268 269# Exceptions seenthe terminal is my friend, and shows AssertionError
AssertionError: datetime.date(YYYY, MM, DD) != []where
YYYYis the current year,MMis the current month andDDis the current date. Progress!When I called
datetime.date()it asked for theyearargument, and the result of the call todatetime.date.today()isdatetime.date(YYYY, MM, DD)which looks like an instance of the datetime.date object with input. I use the dir built-in function to show its attributes257 # def test_dir_datetime_date_year(self): 258 def test_dir_datetime_date_today(self): 259 self.assertEqual( 260 # dir(datetime.date.year), 261 # datetime.date.year, 262 # datetime.date().year, 263 # datetime.date.today, 264 # datetime.date.today(), 265 dir(datetime.date.today()), 266 [] 267 ) 268 269 270# Exceptions seenthe terminal is my friend, and shows AssertionError with a message about setting self.maxDiff to see the full difference
I set self.maxDiff to None
257 # def test_dir_datetime_date_year(self): 258 def test_dir_datetime_date_today(self): 259 self.maxDiff = None 260 self.assertEqual( 261 # dir(datetime.date.year), 262 # datetime.date.year, 263 # datetime.date().year, 264 # datetime.date.today, 265 # datetime.date.today(), 266 dir(datetime.date.today()), 267 [] 268 ) 269 270 271# Exceptions seenthe terminal shows the entire difference between
realityandmy_expectationand there is ayearattribute because they are the same as the attributes and methods of datetime.dateI change the expectation of the assertion
257 # def test_dir_datetime_date_year(self): 258 def test_dir_datetime_date_today(self): 259 self.maxDiff = None 260 self.assertEqual( 261 # dir(datetime.date.year), 262 # datetime.date.year, 263 # datetime.date().year, 264 # datetime.date.today, 265 # datetime.date.today(), 266 dir(datetime.date.today()), 267 # [] 268 dir(datetime.date) 269 ) 270 271 272# Exceptions seenthe test passes.
test_datetime_date_today_year
It looks like I have a way to get the current year.
RED: make it fail
I add test_datetime_date_today_year to test the year attribute of the result of a call to the today method of the date object of the datetime module (datetime.date.today().year) in tests/test_person.py
268 dir(datetime.date)
269 )
270
271 def test_datetime_date_today_year(self):
272 self.assertEqual(
273 datetime.date.today().year,
274 1900
275 )
276
277
278# Exceptions seen
the terminal is my friend, and shows AssertionError
AssertionError: YYYY != 1900
where YYYY is the current year.
GREEN: make it pass
I change my expectation to match reality and the test passes.
I remove all the datetime tests now that I know datetime.date.today().year works
181 def test_dir_person_instance(self): 182 self.assertEqual( 183 dir( 184 src.person.Person( 185 first_name='first_name', 186 last_name='last_name', 187 sex='M', 188 year_of_birth=2026, 189 ) 190 ), 191 [ 192 '__class__', '__delattr__', '__dict__',202 'last_name', 'say_hello', 'sex', 'year_of_birth', 203 ] 204 ) 205 206 207# Exceptions seen
I have a way to automatically get the current year that will always be correct.
I imagine Python follows this path to get the value of datetime.date.today().year
datetime.date.today().year
└── datetime
└── class date:
│ @staticmethod
└── def today():
├── current_year = YYYY
├── current_month = MM
├── current_day = DD
└── return datetime.date(
current_year, current_month, current_day
)
test age with current year
I change the age calculation in the expectation of the assert_say_hello_works method to datetime.date.today().year
25 def assert_say_hello_works( 26 self, first_name, last_name, 27 year_of_birth, 28 ): 29 self.assertEqual( 30 src.person.say_hello( 31 first_name=first_name, 32 last_name=last_name, 33 year_of_birth=year_of_birth 34 ), 35 ( 36 f'Hello, my name is {first_name}' 37 f' {last_name} and I am' 38 # f' {2026-year_of_birth}.' 39 f' {datetime.date.today().year-year_of_birth}.' 40 ) 41 ) 42 43 def assert_person_can_say_hello( 44 self,first_name, last_name, 45 sex, year_of_birth, 46 ):all the tests are still green.
I change the age calculation in the expectation of the assert_person_can_say_hello method to datetime.date.today().year
43 def assert_person_can_say_hello( 44 self,first_name, last_name, 45 sex, year_of_birth, 46 ): 47 self.assertEqual( 48 src.person.Person( 49 first_name=first_name, 50 last_name=last_name, 51 sex=sex, 52 year_of_birth=year_of_birth, 53 ).say_hello(), 54 ( 55 f'Hello, my name is {first_name}' 56 f' {last_name} and I am' 57 # f' {2026-year_of_birth}.' 58 f' {datetime.date.today().year-year_of_birth}.' 59 ) 60 ) 61 62 def test_joe(self):still green.
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 \ 'use datetime to calculate age'
extract this_year attribute
The assert_say_hello_works and assert_person_can_say_hello methods both call datetime.date.today() to get the year attribute. I can use a object attribute to remove the repetition
I go back to the terminal where the tests are running
I add a object attribute to TestPerson for the current year
6class TestPerson(unittest.TestCase): 7 8 this_year = datetime.date.today().year 9 10 def assert_person_factory_works( 11 self, first_name, last_name, 12 sex, year_of_birth 13 ):I use the attribute for
datetime.date.today().yearin the assert_say_hello_works method27 def assert_say_hello_works( 28 self, first_name, last_name, 29 year_of_birth, 30 ): 31 self.assertEqual( 32 src.person.say_hello( 33 first_name=first_name, 34 last_name=last_name, 35 year_of_birth=year_of_birth 36 ), 37 ( 38 f'Hello, my name is {first_name}' 39 f' {last_name} and I am' 40 # f' {2026-year_of_birth}.' 41 # f' {datetime.date.today().year-year_of_birth}.' 42 f' {self.this_year-year_of_birth}.' 43 ) 44 ) 45 46 def assert_person_can_say_hello( 47 self,first_name, last_name, 48 sex, year_of_birth, 49 ):still green.
I use the attribute for
datetime.date.today().yearin the assert_person_can_say_hello method46 def assert_person_can_say_hello( 47 self,first_name, last_name, 48 sex, year_of_birth, 49 ): 50 self.assertEqual( 51 src.person.Person( 52 first_name=first_name, 53 last_name=last_name, 54 sex=sex, 55 year_of_birth=year_of_birth, 56 ).say_hello(), 57 ( 58 f'Hello, my name is {first_name}' 59 f' {last_name} and I am' 60 # f' {2026-year_of_birth}.' 61 # f' {datetime.date.today().year-year_of_birth}.' 62 f' {self.this_year-year_of_birth}.' 63 ) 64 ) 65 66 def test_joe(self):the tests are still green.
I add a git commit message in the other terminal
git commit -am \ 'extract this_year attribute'
extract calculate_age method
The assert_say_hello_works and assert_person_can_say_hello methods both do a calculation for the age. I can use a method to remove the repetition.
RED: make it fail
I go back to the terminal where the tests are running
I add a method to TestPerson to calculate the age
10 def assert_person_factory_works( 11 self, first_name, last_name, 12 sex, year_of_birth 13 ): 14 self.assertEqual( 15 src.person.person( 16 first_name=first_name, 17 last_name=last_name, 18 sex=sex, 19 year_of_birth=year_of_birth, 20 ), 21 ( 22 f'{first_name}, {last_name},' 23 f' {sex}, {year_of_birth}' 24 ) 25 ) 26 27 def calculate_age(year_of_birth): 28 return self.this_year - year_of_birth 29 30 def assert_say_hello_works( 31 self, first_name, last_name, 32 year_of_birth, 33 ):I use the method for
self.this_year-year_of_birthin the assert_say_hello_works method30 def assert_say_hello_works( 31 self, first_name, last_name, 32 year_of_birth, 33 ): 34 self.assertEqual( 35 src.person.say_hello( 36 first_name=first_name, 37 last_name=last_name, 38 year_of_birth=year_of_birth 39 ), 40 ( 41 f'Hello, my name is {first_name}' 42 f' {last_name} and I am' 43 # f' {2026-year_of_birth}.' 44 # f' {datetime.date.today().year-year_of_birth}.' 45 # f' {self.this_year-year_of_birth}.' 46 f' {self.calculate_age(year_of_birth)}.' 47 ) 48 ) 49 50 def assert_person_can_say_hello( 51 self,first_name, last_name, 52 sex, year_of_birth, 53 ):the terminal is my friend, and shows TypeError
TypeError: TestPerson.calculate_age() takes 1 positional argument but 2 were given
GREEN: make it pass
I add self to the parentheses of calculate_age
10 def assert_person_factory_works(
11 self, first_name, last_name,
12 sex, year_of_birth
13 ):
14 self.assertEqual(
15 src.person.person(
16 first_name=first_name,
17 last_name=last_name,
18 sex=sex,
19 year_of_birth=year_of_birth,
20 ),
21 (
22 f'{first_name}, {last_name},'
23 f' {sex}, {year_of_birth}'
24 )
25 )
26
27 # def calculate_age(year_of_birth):
28 def calculate_age(self, year_of_birth):
29 return self.this_year - year_of_birth
30
31 def assert_say_hello_works(
32 self, first_name, last_name,
33 year_of_birth,
34 ):
the test passes.
REFACTOR: make it better
I remove the commented lines from the assert_say_hello_works method
31 def assert_say_hello_works( 32 self, first_name, last_name, 33 year_of_birth, 34 ): 35 self.assertEqual( 36 src.person.say_hello( 37 first_name=first_name, 38 last_name=last_name, 39 year_of_birth=year_of_birth 40 ), 41 ( 42 f'Hello, my name is {first_name}' 43 f' {last_name} and I am' 44 f' {self.calculate_age(year_of_birth)}.' 45 ) 46 ) 47 48 def assert_person_can_say_hello( 49 self,first_name, last_name, 50 sex, year_of_birth, 51 ):I use the method for
self.this_year-year_of_birthin the assert_person_can_say_hello method48 def assert_person_can_say_hello( 49 self,first_name, last_name, 50 sex, year_of_birth, 51 ): 52 self.assertEqual( 53 src.person.Person( 54 first_name=first_name, 55 last_name=last_name, 56 sex=sex, 57 year_of_birth=year_of_birth, 58 ).say_hello(), 59 ( 60 f'Hello, my name is {first_name}' 61 f' {last_name} and I am' 62 # f' {2026-year_of_birth}.' 63 # f' {datetime.date.today().year-year_of_birth}.' 64 # f' {self.this_year-year_of_birth}.' 65 f' {self.calculate_age(year_of_birth)}.' 66 ) 67 ) 68 69 def test_joe(self):the test is still green.
I remove the commented lines from the assert_person_can_say_hello method
48 def assert_person_can_say_hello( 49 self,first_name, last_name, 50 sex, year_of_birth, 51 ): 52 self.assertEqual( 53 src.person.Person( 54 first_name=first_name, 55 last_name=last_name, 56 sex=sex, 57 year_of_birth=year_of_birth, 58 ).say_hello(), 59 ( 60 f'Hello, my name is {first_name}' 61 f' {last_name} and I am' 62 f' {self.calculate_age(year_of_birth)}.' 63 ) 64 ) 65 66 def test_joe(self):The this_year object attribute is now used in only one place - the calculate_age method. I can call what it points to directly, with no need for the object attribute as a middle man
27 # def calculate_age(year_of_birth): 28 def calculate_age(self, year_of_birth): 29 # return self.this_year - year_of_birth 30 return ( 31 datetime.date.today().year 32 - year_of_birth 33 ) 34 35 def assert_say_hello_works( 36 self, first_name, last_name, 37 year_of_birth, 38 ):I add the staticmethod decorator since calculate_age no longer uses anything that belongs to the TestPerson class
27 # def calculate_age(year_of_birth): 28 @staticmethod 29 def calculate_age(self, year_of_birth):the terminal is my friend, and shows TypeError
TypeError: TestPerson.calculate_age() missing 1 required positional argument: 'year_of_birth'I remove
selffrom the parentheses27 @staticmethod 28 def calculate_age(year_of_birth): 29 # def calculate_age(self, year_of_birth): 30 # return self.this_year - year_of_birth 31 return ( 32 datetime.date.today().year 33 - year_of_birth 34 )the test is green again.
I remove the commented lines from the calculate_age method
27 @staticmethod 28 def calculate_age(year_of_birth): 29 return ( 30 datetime.date.today().year 31 - year_of_birth 32 ) 33 34 def assert_say_hello_works( 35 self, first_name, last_name, 36 year_of_birth, 37 ):I remove the this_year attribute from the TestPerson class since it is no longer used
6class TestPerson(unittest.TestCase): 7 8 def assert_person_factory_works( 9 self, first_name, last_name, 10 sex, year_of_birth 11 ):when
self.calculate_age(year_of_birth)runsself.calculate_age(year_of_birth) └── class TestPerson(unittest.TestCase) │ @staticmethod └── def calculate_age(year_of_birth): └── return ( ┌────── datetime.date.today().year │ - year_of_birth │ ) │ └── datetime └── class date: │ @staticmethod └── def today(): ├── current_year = YYYY ├── current_month = MM ├── current_day = DD └── return datetime.date( current_year, current_month, current_day )using substitution for the return statement
return ( datetime.date.today().year - year_of_birth ) return ( datetime.date(YYYY, MM, DD).year - year_of_birth ) return (YYYY - year_of_birth)where
YYYYis the current year.I add a git commit message in the other terminal
git commit -am \ 'extract calculate_age method'
add calculate_age function
The tests use the right calculation for the age, and the solution still uses a fixed value (2026)
RED: make it fail
I go back to the terminal where the tests are running
I add a function to calculate the age, with the same body as the calculate_age method, in
src/person/__init__.py12 def say_hello(self): 13 return ( 14 f'Hello, my name is {self.first_name}' 15 f' {self.last_name} and I am' 16 f' {2026-self.year_of_birth}.' 17 ) 18 19 20def calculate_age(year_of_birth): 21 return ( 22 datetime.date.today().year 23 - year_of_birth 24 ) 25 26 27def say_hello( 28 first_name, last_name, year_of_birth, 29 ):I use the function in the say_hello method of the Person class
12 def say_hello(self): 13 return ( 14 f'Hello, my name is {self.first_name}' 15 f' {self.last_name} and I am' 16 # f' {2026-self.year_of_birth}.' 17 f' {calculate_age(self.year_of_birth)}.' 18 ) 19 20 21def calculate_age(year_of_birth):the terminal is my friend, and shows NameError
NameError: name 'datetime' is not defined. Did you forget to import 'datetime'?I did.
GREEN: make it pass
I add an import statement at the top of
src/person/__init__.py1import datetime 2 3 4class Person:all tests are green again.
I remove the commented line from the say_hello method
12 def say_hello(self): 13 return ( 14 f'Hello, my name is {self.first_name}' 15 f' {self.last_name} and I am' 16 f' {calculate_age(self.year_of_birth)}.' 17 ) 18 19 20def calculate_age(year_of_birth):I use the function in the say_hello function
30def say_hello( 31 first_name, last_name, year_of_birth, 32 ): 33 return ( 34 f'Hello, my name is {first_name}' 35 f' {last_name} and I am' 36 # f' {2026-year_of_birth}.' 37 f' {calculate_age(year_of_birth)}.' 38 ) 39 40 41def person( 42 first_name, last_name, 43 sex, year_of_birth, 44 ):still green.
I remove the commented line from the say_hello function
30def say_hello( 31 first_name, last_name, year_of_birth, 32 ): 33 return ( 34 f'Hello, my name is {first_name}' 35 f' {last_name} and I am' 36 f' {calculate_age(year_of_birth)}.' 37 ) 38 39 40def person( 41 first_name, last_name, 42 sex, year_of_birth, 43 ):I change the calculation in the calculate_age method to make sure the tests work, in
tests/test_person.py25 @staticmethod 26 def calculate_age(year_of_birth): 27 return 1900 - year_of_birth 28 return ( 29 datetime.date.today().year 30 - year_of_birth 31 ) 32 33 def assert_say_hello_works( 34 self, first_name, last_name, 35 year_of_birth, 36 ):The terminal is my friend, and shows AssertionError for all four people.
The ages of the expectations are all negative numbers, since I used a year that is earlier than the dates of birth except for
john. This is a problem.The results of the call all have the right age. Lovely!
I change the calculation in the calculate_age method back
25 @staticmethod 26 def calculate_age(year_of_birth): 27 return ( 28 datetime.date.today().year 29 - year_of_birth 30 ) 31 32 def test_joe(self):green again.
I add a git commit message in the other terminal
git commit -am \ 'add calculate_age function'
test_when_person_is_older_than_120
I want the calculate_age function to make sure that the age of the person is not more than 120 because I do not know that there are any people alive older than that, yet. For example john smith has a year_of_birth of 1580 which makes him too old to be alive.
RED: make it fail
I go back to the terminal where the tests are running
I add a variable with an assert statement to the calculate_age function in
src/person/__init__.py23def calculate_age(year_of_birth): 24 # return ( 25 age = ( 26 datetime.date.today().year 27 - year_of_birth 28 ) 29 assert age <= 120 30 return age 31 32 33def say_hello( 34 first_name, last_name, year_of_birth, 35):the terminal is my friend, and shows AssertionError
E AssertionErroranother problem, the error message does not tell me much. At least the
short test summary infoshows me what test the error happened inFAILED ...::TestPerson::test_john - AssertionError
GREEN: make it pass
I change the value of year_of_birth in test_john in tests/test_person.py
119 def test_john(self):
120 first_name = 'john'
121 last_name = 'smith'
122 sex = 'M'
123 # year_of_birth = 1580
124 year_of_birth = 1980
125
126 self.assert_person_factory_works(
127 first_name=first_name,
128 last_name=last_name,
129 sex=sex,
130 year_of_birth=year_of_birth,
131 )
the test passes.
REFACTOR: make it better
I remove the commented line from test_john
119 def test_john(self): 120 first_name = 'john' 121 last_name = 'smith' 122 sex = 'M' 123 year_of_birth = 1980 124 125 self.assert_person_factory_works( 126 first_name=first_name, 127 last_name=last_name, 128 sex=sex, 129 year_of_birth=year_of_birth, 130 )I add a test for when the person is older than
120145def test_mary(self): 146 ...164 self.assert_person_can_say_hello( 165 first_name=first_name, 166 last_name=last_name, 167 sex=sex, 168 year_of_birth=year_of_birth, 169 ) 170 171def test_when_person_is_older_than_120(self): 172 src.person.Person( 173 first_name='first_name', 174 last_name='last_name', 175 sex='M', 176 year_of_birth=datetime.date.today().year-121 177 ).say_hello() 178 179def test_dir_person_class(self):the terminal is my friend, and shows AssertionError
E AssertionErrorI use
datetime.date.today().year - 121to make sure the person is always older than120The
short test summary infoshows me what test the error happened inFAILED ...test_when_person_is_older_than_120 - AssertionError
I remove the call to the say_hello method then add a comment about the failure
171 def test_when_person_is_older_than_120(self): 172 src.person.Person( 173 first_name='first_name', 174 last_name='last_name', 175 sex='M', 176 year_of_birth=datetime.date.today().year-121 177 ) 178 # ).say_hello() fails 179 # because age > 120 180 181 def test_dir_person_class(self):the test is green because there are no calls that cause an Exception.
I remove the commented line from the calculate_age function in
src/person/__init__.py23def calculate_age(year_of_birth): 24 age = ( 25 datetime.date.today().year 26 - year_of_birth 27 ) 28 assert age <= 120 29 return age 30 31 32def say_hello( 33 first_name, last_name, year_of_birth, 34 ):I add a git commit message in the other terminal
git commit -am \ 'add test_when_person_is_older_than_120'
test_when_year_of_birth_is_the_future
I want the calculate_age function to also make sure that the year of birth of a person is before the current year.
RED: make it fail
I go back to the terminal where the tests are running
I add a new test for when
year_of_birthis in the future, intests/test_person.py178 # ).say_hello() fails 179 # because age > 120 180 181 def test_when_year_of_birth_is_the_future(self): 182 self.assertEqual( 183 src.person.Person( 184 first_name='first_name', 185 last_name='last_name', 186 sex='F', 187 year_of_birth=datetime.date.today().year+1, 188 ).say_hello(), 189 AssertionError 190 ) 191 192 def test_dir_person_class(self):the terminal is my friend, and shows AssertionError
AssertionError: 'Hello, my name is first_name last_name and I am -1.' != <class 'AssertionError'>I use
datetime.date.today().year + 1for all the years of birth in the future.
GREEN: make it pass
I add an assertion to the calculate age function to make sure that it never returns a number that is less than
0, insrc/person/__init__.py23def calculate_age(year_of_birth): 24 age = ( 25 datetime.date.today().year 26 - year_of_birth 27 ) 28 assert age >= 0 29 assert age <= 120 30 return age 31 32 33def say_hello( 34 first_name, last_name, year_of_birth 35 ):the terminal is my friend, and shows AssertionError
E AssertionErrorthe error message is still a problem. The
short test summary infoshows me what test the error happened inFAILED ...test_when_year_of_birth_is_the_future - AssertionErrorI remove the assertion and call to the say_hello method in test_when_year_of_birth_is_the_future, in
tests/test_person.py181 def test_when_year_of_birth_is_the_future(self): 182 # self.assertEqual( 183 src.person.Person( 184 first_name='first_name', 185 last_name='last_name', 186 sex='F', 187 year_of_birth=datetime.date.today().year+1, 188 ) 189 # ).say_hello(), 190 # AssertionError 191 # ) 192 193 def test_dir_person_class(self):the test is green because there are no calls that cause an Exception.
I remove the commented lines from test_when_year_of_birth_is_the_future then add a comment about the test
181 def test_when_year_of_birth_is_the_future(self): 182 src.person.Person( 183 first_name='first_name', 184 last_name='last_name', 185 sex='F', 186 year_of_birth=datetime.date.today().year+1, 187 ) 188 # ).say_hello() fails 189 # because age < 0 190 191 def test_dir_person_class(self):I add a git commit message in the other terminal
git commit -am \ 'add test_when_year_of_birth_is_the_future'
extract this_year attribute again
The calculate_age, test_when_person_is_older_than_120 and test_when_year_of_birth_is_the_future all call datetime.date.today() to get the year attribute.
I made a object attribute for this_year earlier to remove the repetition which ended up in the calculate_age method, I can make it again to remove repetition from these three methods
RED: make it fail
I go back to the terminal where the tests are running
I add back the object attribute for the current year to the TestPerson class
6class TestPerson(unittest.TestCase): 7 8 this_year = datetime.date.today().year 9 10 def assert_person_factory_works( 11 self, first_name, last_name, 12 sex, year_of_birth 13 ):I use the attribute for
datetime.date.today().yearin the calculate_age method27 @staticmethod 28 def calculate_age(year_of_birth): 29 return ( 30 # datetime.date.today().year 31 self.this_year 32 - year_of_birth 33 ) 34 35 def assert_say_hello_works( 36 self, first_name, last_name, 37 year_of_birth, 38 ):the terminal is my friend, and shows NameError
GREEN: make it pass
I comment out the staticmethod decorator then add self to the parentheses of the calculate_age method
27 # @staticmethod
28 # def calculate_age(year_of_birth):
29 def calculate_age(self, year_of_birth):
30 return (
31 # datetime.date.today().year
32 self.this_year
33 - year_of_birth
34 )
the tests are green again.
REFACTOR: make it better
I remove the commented lines from the calculate_age method
27def calculate_age(self, year_of_birth): 28 return self.this_year - year_of_birth 29 30def assert_say_hello_works( 31 self, first_name, last_name, 32 year_of_birth, 33 ):I use the attribute for
datetime.date.today().yearin test_when_person_is_older_than_120169 def test_when_person_is_older_than_120(self): 170 src.person.Person( 171 first_name='first_name', 172 last_name='last_name', 173 sex='M', 174 # year_of_birth=datetime.date.today().year-121 175 year_of_birth=self.this_year-121 176 ) 177 # ).say_hello() fails 178 # because age > 120the tests are still green.
I remove the new commented line from test_when_person_is_older_than_120
169 def test_when_person_is_older_than_120(self): 170 src.person.Person( 171 first_name='first_name', 172 last_name='last_name', 173 sex='M', 174 year_of_birth=self.this_year-121 175 ) 176 # ).say_hello() fails 177 # because age > 120 178 179 def test_when_year_of_birth_is_the_future(self):I use the attribute for
datetime.date.today().yearin test_when_year_of_birth_is_the_future179 def test_when_year_of_birth_is_the_future(self): 180 src.person.Person( 181 first_name='first_name', 182 last_name='last_name', 183 sex='F', 184 # year_of_birth=datetime.date.today().year+1, 185 year_of_birth=self.this_year+1, 186 ) 187 # ).say_hello() fails 188 # because age < 0 189 190 def test_dir_person_class(self):still green.
I remove the new commented line from test_when_year_of_birth_is_the_future
179 def test_when_year_of_birth_is_the_future(self): 180 src.person.Person( 181 first_name='first_name', 182 last_name='last_name', 183 sex='F', 184 year_of_birth=self.this_year+1, 185 ) 186 # ).say_hello() fails 187 # because age < 0 188 189 def test_dir_person_class(self):I add a git commit message in the other terminal
git commit -am \ 'extract this_year attribute'
test_when_year_of_birth_is_not_an_integer
I want the calculate_age function to make sure that the value for year_of_birth is an integer (a whole number without decimals).
RED: make it fail
I go back to the terminal where the tests are running
I add a new test for when
year_of_birthis not an integer186 # ).say_hello() fails 187 # because age < 0 188 189 def test_when_year_of_birth_is_not_an_integer(self): 190 src.person.Person( 191 first_name='first_name', 192 last_name='last_name', 193 sex='M', 194 ).say_hello() 195 196 def test_dir_person_class(self):the terminal is my friend, and shows TypeError
TypeError: Person.__init__() missing 1 required positional argument: 'year_of_birth'
GREEN: make it pass
I make
year_of_birthan optional argument in the constructor method of the Person class insrc/person/__init__.py4class Person: 5 6 def __init__( 7 self, first_name, last_name, 8 # sex, year_of_birth, 9 sex, year_of_birth=None, 10 ):TypeError: unsupported operand type(s) for -: 'int' and 'NoneType'because I cannot do Arithmetic with None.
I add an assertion with the isinstance built-in function to make sure the calculate age function only works with integers
24def calculate_age(year_of_birth): 25 assert isinstance(year_of_birth, int) 26 age = ( 27 datetime.date.today().year 28 - year_of_birth 29 ) 30 assert age >= 0 31 assert age <= 120 32 return age 33 34 35def say_hello( 36 first_name, last_name, year_of_birth 37 ):the terminal is my friend, and shows AssertionError
E AssertionErroranother bad error message. The
short test summary infoshows me what test the error happened inFAILED ...test_when_year_of_birth_is_not_an_integer - AssertionErrorI add a comment, then add False as the value for if a boolean is given as the value for the
year_of_birthparameter in test_when_year_of_birth_is_not_an_integer, intests/test_person.py189 def test_when_year_of_birth_is_not_an_integer(self): 190 src.person.Person( 191 first_name='first_name', 192 last_name='last_name', 193 sex='M', 194 # year_of_birth=None, # fails 195 year_of_birth=False, 196 ).say_hello() 197 198 def test_dir_person_class(self):the terminal shows AssertionError for the age being greater than
120. Wait a minute! I was expecting that to fail atassert isinstance(year_of_birth, int). This means a boolean is also an integer.I change
year_of_birthfrom False to a float189 def test_when_year_of_birth_is_not_an_integer(self): 190 src.person.Person( 191 first_name='first_name', 192 last_name='last_name', 193 sex='M', 194 # year_of_birth=None, # fails 195 year_of_birth=2026.0, 196 ).say_hello() 197 198 def test_dir_person_class(self):the terminal shows AssertionError
I add a comment then change
year_of_birthto a string189 def test_when_year_of_birth_is_not_an_integer(self): 190 src.person.Person( 191 first_name='first_name', 192 last_name='last_name', 193 sex='M', 194 # year_of_birth=None, # fails 195 # year_of_birth=2026.0, # fails 196 year_of_birth='2026', 197 ).say_hello() 198 199 def test_dir_person_class(self):the terminal shows AssertionError
I add a comment then change
year_of_birthto a tuple189 def test_when_year_of_birth_is_not_an_integer(self): 190 src.person.Person( 191 first_name='first_name', 192 last_name='last_name', 193 sex='M', 194 # year_of_birth=None, # fails 195 # year_of_birth=2026.0, # fails 196 # year_of_birth='2026', # fails 197 year_of_birth=(2026,), 198 ).say_hello() 199 200 def test_dir_person_class(self):I comment out the call to the say_hello method then add a comment about the test
189 def test_when_year_of_birth_is_not_an_integer(self): 190 src.person.Person( 191 first_name='first_name', 192 last_name='last_name', 193 sex='M', 194 # year_of_birth=None, # fails 195 # year_of_birth=2026.0, # fails 196 # year_of_birth='2026', # fails 197 # year_of_birth=(2026,), # fails 198 ) 199 # ).say_hello() fails 200 # because year_of_birth is not an integer 201 202 def test_dir_person_class(self):the test is green because there are no calls that cause an Exception.
I remove the commented line from 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 ):I add a git commit message in the other terminal
git commit -am \ 'add test_when_year_of_birth_is_not_an_integer'
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 the datetime library to automatically get the current year for the calculation of a person’s age.
I can use assertions to make sure certain conditions are met before a program does something.
My tests have a new problem - when they cause an Exception the test stops in a RED state. My solution was to add notes and comment out the problems, which means the only way to know that the code causes the Exception is to remove the comments. 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.