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 have these tests by the end of the chapter

person/tests/test_person.py
  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
 58    def test_jane(self):
 59        first_name = 'jane'
 60        last_name = 'doe'
 61        sex = 'F'
 62        year_of_birth = 1991
 63
 64        reality = src.person.factory(
 65            first_name=first_name,
 66            last_name=last_name,
 67            sex=sex,
 68            year_of_birth=year_of_birth,
 69        )
 70        my_expectation = (
 71            f'{first_name}, {last_name},'
 72            f' {sex}, {year_of_birth}'
 73        )
 74        assert reality == my_expectation
 75        self.assertEqual(reality, my_expectation)
 76
 77        reality = src.person.say_hello(
 78            first_name=first_name,
 79            last_name=last_name,
 80            year_of_birth=year_of_birth,
 81        )
 82        my_expectation = (
 83            f'Hello, my name is {first_name}'
 84            f' {last_name} and I am'
 85            f' {self.calculate_age(year_of_birth)}.'
 86        )
 87        assert reality == my_expectation
 88        self.assertEqual(reality, my_expectation)
 89
 90        jane = src.person.Person(
 91            first_name=first_name,
 92            last_name=last_name,
 93            sex=sex,
 94            year_of_birth=year_of_birth,
 95        )
 96
 97        reality = jane.say_hello()
 98        assert reality == my_expectation
 99        self.assertEqual(reality, my_expectation)
100
101    def test_john(self):
102        first_name = 'john'
103        last_name = 'smith'
104        sex = 'M'
105        year_of_birth = 1980
106        # year_of_birth = 1580
107        # raises AssertionError
108        # because older than 120
109
110        reality = src.person.factory(
111            first_name=first_name,
112            last_name=last_name,
113            sex=sex,
114            year_of_birth=year_of_birth,
115        )
116        my_expectation = (
117            f'{first_name}, {last_name},'
118            f' {sex}, {year_of_birth}'
119        )
120        assert reality == my_expectation
121        self.assertEqual(reality, my_expectation)
122
123        reality = src.person.say_hello(
124            first_name=first_name,
125            last_name=last_name,
126            year_of_birth=year_of_birth,
127        )
128        my_expectation = (
129            f'Hello, my name is {first_name}'
130            f' {last_name} and I am'
131            f' {self.calculate_age(year_of_birth)}.'
132        )
133        assert reality == my_expectation
134        self.assertEqual(reality, my_expectation)
135
136        john = src.person.Person(
137            first_name=first_name,
138            last_name=last_name,
139            sex=sex,
140            year_of_birth=year_of_birth,
141        )
142
143        reality = john.say_hello()
144        assert reality == my_expectation
145        self.assertEqual(reality, my_expectation)
146
147    def test_mary(self):
148        first_name = 'mary'
149        last_name = 'public'
150        sex = 'F'
151        year_of_birth = 2000
152
153        reality = src.person.factory(
154            first_name=first_name,
155            last_name=last_name,
156            sex=sex,
157            year_of_birth=year_of_birth,
158        )
159        my_expectation = (
160            f'{first_name}, {last_name},'
161            f' {sex}, {year_of_birth}'
162        )
163        assert reality == my_expectation
164        self.assertEqual(reality, my_expectation)
165
166        reality = src.person.say_hello(
167            first_name=first_name,
168            last_name=last_name,
169            year_of_birth=year_of_birth,
170        )
171        my_expectation = (
172            f'Hello, my name is {first_name}'
173            f' {last_name} and I am'
174            f' {self.calculate_age(year_of_birth)}.'
175        )
176        assert reality == my_expectation
177        self.assertEqual(reality, my_expectation)
178
179        mary = src.person.Person(
180            first_name=first_name,
181            last_name=last_name,
182            sex=sex,
183            year_of_birth=year_of_birth,
184        )
185
186        reality = mary.say_hello()
187        assert reality == my_expectation
188        self.assertEqual(reality, my_expectation)
189
190    def test_when_year_of_birth_is_not_an_integer(self):
191        src.person.Person(
192            first_name='first_name',
193            last_name='last_name',
194            sex='M',
195            # year_of_birth=None,    # fails
196            # year_of_birth=2026.0,  # fails
197            # year_of_birth='2026',  # fails
198            # year_of_birth=(2026,), # fails
199        )
200
201    def test_dir_person_class(self):
202        reality = dir(src.person.Person)
203        my_expectation = [
204            '__class__',
205            '__delattr__',
206            '__dict__',
207            '__dir__',
208            '__doc__',
209            '__eq__',
210            '__firstlineno__',
211            '__format__',
212            '__ge__',
213            '__getattribute__',
214            '__getstate__',
215            '__gt__',
216            '__hash__',
217            '__init__',
218            '__init_subclass__',
219            '__le__',
220            '__lt__',
221            '__module__',
222            '__ne__',
223            '__new__',
224            '__reduce__',
225            '__reduce_ex__',
226            '__repr__',
227            '__setattr__',
228            '__sizeof__',
229            '__static_attributes__',
230            '__str__',
231            '__subclasshook__',
232            '__weakref__',
233            'say_hello'
234        ]
235        assert reality == my_expectation
236        self.assertEqual(reality, my_expectation)
237
238    def test_dir_person_instance(self):
239        an_instance_of_person = src.person.Person(
240            first_name='first_name',
241            last_name='last_name',
242            sex='M',
243            year_of_birth=2026,
244        )
245
246        reality = dir(an_instance_of_person)
247        my_expectation = [
248            '__class__',
249            '__delattr__',
250            '__dict__',
251            '__dir__',
252            '__doc__',
253            '__eq__',
254            '__firstlineno__',
255            '__format__',
256            '__ge__',
257            '__getattribute__',
258            '__getstate__',
259            '__gt__',
260            '__hash__',
261            '__init__',
262            '__init_subclass__',
263            '__le__',
264            '__lt__',
265            '__module__',
266            '__ne__',
267            '__new__',
268            '__reduce__',
269            '__reduce_ex__',
270            '__repr__',
271            '__setattr__',
272            '__sizeof__',
273            '__static_attributes__',
274            '__str__',
275            '__subclasshook__',
276            '__weakref__',
277            'first_name',
278            'last_name',
279            'say_hello',
280            'sex',
281            'year_of_birth',
282        ]
283        assert reality == my_expectation
284        self.assertEqual(reality, my_expectation)
285
286
287# Exceptions seen
288# AssertionError
289# NameError
290# TypeError
291# AttributeError
292# SyntaxError

open the project

  • I open a terminal

  • I change directory to the project

    cd person
    
  • I open test_person.py from the tests folder

  • I use pytest-watcher to run the tests automatically

    uv run pytest-watcher . --now
    

    the 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 test_person.py

    262        self.assertEqual(reality, my_expectation)
    263
    264    def test_dir_datetime(self):
    265        reality = dir(datetime)
    266        my_expectation = []
    267        self.assertEqual(reality, my_expectation)
    268
    269
    270# Exceptions seen
    

    the 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 my_expectation and remove the extra characters

    263        self.assertEqual(reality, my_expectation)
    264
    265    def test_dir_datetime(self):
    266        reality = dir(datetime)
    267        my_expectation = [
    268            'MAXYEAR',
    269            'MINYEAR',
    270            'UTC',
    271            '__all__',
    272            '__builtins__',
    273            '__cached__',
    274            '__doc__',
    275            '__file__',
    276            '__loader__',
    277            '__name__',
    278            '__package__',
    279            '__spec__',
    280            'date',
    281            'datetime',
    282            'datetime_CAPI',
    283            'time',
    284            'timedelta',
    285            'timezone',
    286            'tzinfo'
    287        ]
    288        self.assertEqual(reality, my_expectation)
    289
    290
    291# Exceptions seen
    

    the test passes because when import datetime runs, 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 in test_person.py as datetime.

    This means that there is a file or folder on the computer named datetime that got added when I installed Python.

    Caution

    Your list of attributes and methods may be different depending on your Python version


test_dir_datetime_date

A few names stand out in the list of attributes and methods of datetime

  • date - I assume this handles dates

  • time - I assume this handles time

  • datetime - 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 test_person.py

288        self.assertEqual(reality, my_expectation)
289
290    def test_dir_datetime_date(self):
291        reality = dir(datetime.date)
292        my_expectation = []
293        self.assertEqual(reality, my_expectation)
294
295
296# 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

    288        self.assertEqual(reality, my_expectation)
    289
    290    def test_dir_datetime_date(self):
    291        reality = dir(datetime.date)
    292        my_expectation = []
    293        self.maxDiff = None
    294        self.assertEqual(reality, my_expectation)
    295
    296
    297# Exceptions seen
    
  • I copy (ctrl/command+c) the values from the terminal, paste (ctrl/command+v) them as my_expectation and remove the extra characters

    290    def test_dir_datetime_date(self):
    291        reality = dir(datetime.date)
    292        my_expectation = [
    293            '__add__', '__class__', '__delattr__',
    294            '__dir__', '__doc__', '__eq__',
    295            '__format__', '__ge__', '__getattribute__',
    296            '__getstate__', '__gt__', '__hash__',
    297            '__init__', '__init_subclass__', '__le__',
    298            '__lt__', '__ne__', '__new__', '__radd__',
    299            '__reduce__', '__reduce_ex__',
    300            '__replace__', '__repr__', '__rsub__',
    301            '__setattr__', '__sizeof__', '__str__',
    302            '__sub__', '__subclasshook__', 'ctime',
    303            'day', 'fromisocalendar', 'fromisoformat',
    304            'fromordinal', 'fromtimestamp',
    305            'isocalendar', 'isoformat', 'isoweekday',
    306            'max', 'min', 'month', 'replace',
    307            'resolution', 'strftime', 'strptime',
    308            'timetuple', 'today', 'toordinal',
    309            'weekday', 'year'
    310        ]
    311        self.maxDiff = None
    312        self.assertEqual(reality, my_expectation)
    313
    314
    315# Exceptions seen
    

    the 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 test_person.py

312        self.assertEqual(reality, my_expectation)
313
314    def test_dir_datetime_date_year(self):
315        reality = dir(datetime.date.year)
316        my_expectation = []
317        self.assertEqual(reality, my_expectation)
318
319
320# 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 value of reality to datetime.date.year

    314    def test_dir_datetime_date_year(self):
    315        # reality = dir(datetime.date.year)
    316        reality = datetime.date.year
    317        my_expectation = []
    318        self.assertEqual(reality, my_expectation)
    319
    320
    321# Exceptions seen
    

    the terminal is my friend, and shows AssertionError

    AssertionError:
        <attribute 'year' of 'datetime.date' objects>
     != []
    
  • I try calling date

    314    def test_dir_datetime_date_year(self):
    315        # reality = dir(datetime.date.year)
    316        # reality = datetime.date.year
    317        reality = datetime.date().year
    318        my_expectation = []
    319        self.assertEqual(reality, my_expectation)
    320
    321
    322# Exceptions seen
    

    the terminal is my friend, and shows TypeError

    TypeError: function missing
               required argument 'year' (pos 1)
    

    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 test_person.py

312          self.assertEqual(reality, my_expectation)
313
314      def test_dir_datetime_date_today(self):
315          # reality = dir(datetime.date.year)
316          # reality = datetime.date.year
317          # reality = datetime.date().year
318          reality = datetime.date.today
319          my_expectation = []
320          self.assertEqual(reality, my_expectation)
321
322
323  # 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 change the value of reality to a call to datetime.date.today

    314    def test_dir_datetime_date_today(self):
    315        # reality = dir(datetime.date.year)
    316        # reality = datetime.date.year
    317        # reality = datetime.date().year
    318        # reality = datetime.date.today
    319        reality = datetime.date.today()
    320        my_expectation = []
    321        self.assertEqual(reality, my_expectation)
    322
    323
    324# Exceptions seen
    

    the terminal is my friend, and shows AssertionError

    AssertionError: datetime.date(YYYY, MM, DD) != []
    

    where YYYY is the current year, MM is the current month and DD is the current date. Progress!

  • When I called datetime.date() it asked for the year argument, and the result of the call is datetime.date(YYYY, MM, DD) which looks like a call to datetime.date(). I wonder if it also has a year attribute

    314    def test_dir_datetime_date_today(self):
    315        # reality = dir(datetime.date.year)
    316        # reality = datetime.date.year
    317        # reality = datetime.date().year
    318        # reality = datetime.date.today
    319        # reality = datetime.date.today()
    320        reality = dir(datetime.date.today())
    321        my_expectation = []
    322        self.assertEqual(reality, my_expectation)
    323
    324
    325# Exceptions seen
    

    the 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

    314    def test_dir_datetime_date_today(self):
    315        # reality = dir(datetime.date.year)
    316        # reality = datetime.date.year
    317        # reality = datetime.date().year
    318        # reality = datetime.date.today
    319        # reality = datetime.date.today()
    320        reality = dir(datetime.date.today())
    321        my_expectation = []
    322        self.maxDiff = None
    323        self.assertEqual(reality, my_expectation)
    324
    325
    326# Exceptions seen
    

    the terminal shows the entire difference between reality and my_expectation and there is a year attribute because they are the same as the attributes and methods of datetime.date

  • I change my_expectation

    314    def test_dir_datetime_date_today(self):
    315        # reality = dir(datetime.date.year)
    316        # reality = datetime.date.year
    317        # reality = datetime.date().year
    318        # reality = datetime.date.today
    319        # reality = datetime.date.today()
    320        reality = dir(datetime.date.today())
    321        # my_expectation = []
    322        my_expectation = dir(datetime.date)
    323        self.maxDiff = None
    324        self.assertEqual(reality, my_expectation)
    325
    326
    327# Exceptions seen
    

    the 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 class of the datetime module (datetime.date.today().year) in test_person.py

324          self.assertEqual(reality, my_expectation)
325
326      def test_datetime_date_today_year(self):
327          reality = datetime.date.today().year
328          my_expectation = 1900
329          self.assertEqual(reality, my_expectation)
330
331
332  # 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

    256            'first_name',
    257            'last_name',
    258            'say_hello',
    259            'sex',
    260            'year_of_birth',
    261        ]
    262        assert reality == my_expectation
    263        self.assertEqual(reality, my_expectation)
    264
    265
    266# Exceptions seen
    

I have a way to automatically get the current year that will always be correct.


test age with current year

  • I change the age calculation in my_expectation of say_hello in test_joe with datetime.date.today().year

    27      reality = src.person.say_hello(
    28          first_name=first_name,
    29          last_name=last_name,
    30          year_of_birth=year_of_birth,
    31      )
    32      my_expectation = (
    33          f'Hello, my name is {first_name}'
    34          f' {last_name} and I am'
    35          # f' {2026-year_of_birth}.'
    36          f' {datetime.date.today().year-year_of_birth}.'
    37      )
    38      assert reality == my_expectation
    39      self.assertEqual(reality, my_expectation)
    40
    41      joe = src.person.Person(
    

    the test is still green.

  • I change the age calculation in my_expectation of say_hello in test_jane with datetime.date.today().year

    71        reality = src.person.say_hello(
    72            first_name=first_name,
    73            last_name=last_name,
    74            year_of_birth=year_of_birth,
    75        )
    76        my_expectation = (
    77            f'Hello, my name is {first_name}'
    78            f' {last_name} and I am'
    79            # f' {2026-year_of_birth}.'
    80            f' {datetime.date.today().year-year_of_birth}.'
    81        )
    82        assert reality == my_expectation
    83        self.assertEqual(reality, my_expectation)
    84
    85        jane = src.person.Person(
    

    still green.

  • I change the age calculation in my_expectation of say_hello in test_john with datetime.date.today().year

    115        reality = src.person.say_hello(
    116            first_name=first_name,
    117            last_name=last_name,
    118            year_of_birth=year_of_birth,
    119        )
    120        my_expectation = (
    121            f'Hello, my name is {first_name}'
    122            f' {last_name} and I am'
    123            # f' {2026-year_of_birth}.'
    124            f' {datetime.date.today().year-year_of_birth}.'
    125        )
    126        assert reality == my_expectation
    127        self.assertEqual(reality, my_expectation)
    128
    129        john = src.person.Person(
    

    green.

  • I change the age calculation in my_expectation of say_hello in test_mary with datetime.date.today().year

    159        reality = src.person.say_hello(
    160            first_name=first_name,
    161            last_name=last_name,
    162            year_of_birth=year_of_birth,
    163        )
    164        my_expectation = (
    165            f'Hello, my name is {first_name}'
    166            f' {last_name} and I am'
    167            # f' {2026-year_of_birth}.'
    168            f' {datetime.date.today().year-year_of_birth}.'
    169        )
    170        assert reality == my_expectation
    171        self.assertEqual(reality, my_expectation)
    172
    173        mary = src.person.Person(
    

    still green.

  • I open a new terminal then make sure I am in the person folder

    cd person
    
  • I add a git commit message in the new terminal

    git commit -am \
    'use datetime to calculate age'
    

extract this_year attribute

Each test calls datetime.date.today() to get the year attribute.

  • I go back to the terminal where the tests are running

  • I add a class attribute to TestPerson for the current year

     6class TestPerson(unittest.TestCase):
     7
     8    this_year = datetime.date.today().year
     9
    10    def test_joe(self):
    
  • I use the attribute for datetime.date.today().year in test_joe

    29        reality = src.person.say_hello(
    30            first_name=first_name,
    31            last_name=last_name,
    32            year_of_birth=year_of_birth,
    33        )
    34        my_expectation = (
    35            f'Hello, my name is {first_name}'
    36            f' {last_name} and I am'
    37            # f' {2026-year_of_birth}.'
    38            # f' {datetime.date.today().year-year_of_birth}.'
    39            f' {self.this_year-year_of_birth}.'
    40        )
    41        assert reality == my_expectation
    42        self.assertEqual(reality, my_expectation)
    43
    44        joe = src.person.Person(
    

    still green.

  • I use the attribute for datetime.date.today().year in test_jane

    74        reality = src.person.say_hello(
    75            first_name=first_name,
    76            last_name=last_name,
    77            year_of_birth=year_of_birth,
    78        )
    79        my_expectation = (
    80            f'Hello, my name is {first_name}'
    81            f' {last_name} and I am'
    82            # f' {2026-year_of_birth}.'
    83            # f' {datetime.date.today().year-year_of_birth}.'
    84            f' {self.this_year-year_of_birth}.'
    85        )
    86        assert reality == my_expectation
    87        self.assertEqual(reality, my_expectation)
    88
    89        jane = src.person.Person(
    

    green.

  • I use the attribute for datetime.date.today().year in test_john

    119        reality = src.person.say_hello(
    120            first_name=first_name,
    121            last_name=last_name,
    122            year_of_birth=year_of_birth,
    123        )
    124        my_expectation = (
    125            f'Hello, my name is {first_name}'
    126            f' {last_name} and I am'
    127            # f' {2026-year_of_birth}.'
    128            # f' {datetime.date.today().year-year_of_birth}.'
    129            f' {self.this_year-year_of_birth}.'
    130        )
    131        assert reality == my_expectation
    132        self.assertEqual(reality, my_expectation)
    133
    134        john = src.person.Person(
    

    still green.

  • I use the attribute for datetime.date.today().year in test_mary

    164        reality = src.person.say_hello(
    165            first_name=first_name,
    166            last_name=last_name,
    167            year_of_birth=year_of_birth,
    168        )
    169        my_expectation = (
    170            f'Hello, my name is {first_name}'
    171            f' {last_name} and I am'
    172            # f' {2026-year_of_birth}.'
    173            # f' {datetime.date.today().year-year_of_birth}.'
    174            f' {self.this_year-year_of_birth}.'
    175        )
    176        assert reality == my_expectation
    177        self.assertEqual(reality, my_expectation)
    178
    179        mary = src.person.Person(
    

    the test is still green.

  • I add a git commit message in the other terminal

    git commit -am \
    'extract this_year attribute'
    

extract calculate_age method

Each test does a calculation for the age. I can make 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

     6class TestPerson(unittest.TestCase):
     7
     8    this_year = datetime.date.today().year
     9
    10    def calculate_age(year_of_birth):
    11        return self.this_year - year_of_birth
    12
    13    def test_joe(self):
    
  • I use the method for self.this_year-year_of_birth in test_joe

    32        reality = src.person.say_hello(
    33            first_name=first_name,
    34            last_name=last_name,
    35            year_of_birth=year_of_birth,
    36        )
    37        my_expectation = (
    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            f' {self.calculate_age(year_of_birth)}.'
    44        )
    45        assert reality == my_expectation
    46        self.assertEqual(reality, my_expectation)
    47
    48        joe = src.person.Person(
    

    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

 6  class TestPerson(unittest.TestCase):
 7
 8      this_year = datetime.date.today().year
 9
10      # def calculate_age(year_of_birth):
11      def calculate_age(self, year_of_birth):
12          return self.this_year - year_of_birth
13
14      def test_joe(self):

the test passes.


REFACTOR: make it better


  • I remove the commented lines from test_joe

    33        reality = src.person.say_hello(
    34            first_name=first_name,
    35            last_name=last_name,
    36            year_of_birth=year_of_birth,
    37        )
    38        my_expectation = (
    39            f'Hello, my name is {first_name}'
    40            f' {last_name} and I am'
    41            f' {self.calculate_age(year_of_birth)}.'
    42        )
    43        assert reality == my_expectation
    44        self.assertEqual(reality, my_expectation)
    45
    46        joe = src.person.Person(
    
  • I use the method for self.this_year-year_of_birth in test_jane

    76        reality = src.person.say_hello(
    77            first_name=first_name,
    78            last_name=last_name,
    79            year_of_birth=year_of_birth,
    80        )
    81        my_expectation = (
    82            f'Hello, my name is {first_name}'
    83            f' {last_name} and I am'
    84            # f' {2026-year_of_birth}.'
    85            # f' {datetime.date.today().year-year_of_birth}.'
    86            # f' {self.this_year-year_of_birth}.'
    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(
    

    the test is still green.

  • I remove the commented lines from test_jane

    76        reality = src.person.say_hello(
    77            first_name=first_name,
    78            last_name=last_name,
    79            year_of_birth=year_of_birth,
    80        )
    81        my_expectation = (
    82            f'Hello, my name is {first_name}'
    83            f' {last_name} and I am'
    84            f' {self.calculate_age(year_of_birth)}.'
    85        )
    86        assert reality == my_expectation
    87        self.assertEqual(reality, my_expectation)
    88
    89        jane = src.person.Person(
    
  • I use the method for self.this_year-year_of_birth in test_john

    119        reality = src.person.say_hello(
    120            first_name=first_name,
    121            last_name=last_name,
    122            year_of_birth=year_of_birth,
    123        )
    124        my_expectation = (
    125            f'Hello, my name is {first_name}'
    126            f' {last_name} and I am'
    127            # f' {2026-year_of_birth}.'
    128            # f' {datetime.date.today().year-year_of_birth}.'
    129            # f' {self.this_year-year_of_birth}.'
    130            f' {self.calculate_age(year_of_birth)}.'
    131        )
    132        assert reality == my_expectation
    133        self.assertEqual(reality, my_expectation)
    134
    135        john = src.person.Person(
    

    still green.

  • I remove the commented lines from test_john

    119        reality = src.person.say_hello(
    120            first_name=first_name,
    121            last_name=last_name,
    122            year_of_birth=year_of_birth,
    123        )
    124        my_expectation = (
    125            f'Hello, my name is {first_name}'
    126            f' {last_name} and I am'
    127            f' {self.calculate_age(year_of_birth)}.'
    128        )
    129        assert reality == my_expectation
    130        self.assertEqual(reality, my_expectation)
    131
    132        john = src.person.Person(
    
  • I use the method for self.this_year-year_of_birth in test_mary

    162        reality = src.person.say_hello(
    163            first_name=first_name,
    164            last_name=last_name,
    165            year_of_birth=year_of_birth,
    166        )
    167        my_expectation = (
    168            f'Hello, my name is {first_name}'
    169            f' {last_name} and I am'
    170            # f' {2026-year_of_birth}.'
    171            # f' {datetime.date.today().year-year_of_birth}.'
    172            # f' {self.this_year-year_of_birth}.'
    173            f' {self.calculate_age(year_of_birth)}.'
    174        )
    175        assert reality == my_expectation
    176        self.assertEqual(reality, my_expectation)
    177
    178        mary = src.person.Person(
    

    green.

  • I remove the commented lines from test_mary

    162        reality = src.person.say_hello(
    163            first_name=first_name,
    164            last_name=last_name,
    165            year_of_birth=year_of_birth,
    166        )
    167        my_expectation = (
    168            f'Hello, my name is {first_name}'
    169            f' {last_name} and I am'
    170            f' {self.calculate_age(year_of_birth)}.'
    171        )
    172        assert reality == my_expectation
    173        self.assertEqual(reality, my_expectation)
    174
    175        mary = src.person.Person(
    
  • The this_year class attribute is now used in only one place the calculate_age method. I can call what it points to directly

     6class TestPerson(unittest.TestCase):
     7
     8    this_year = datetime.date.today().year
     9
    10    # def calculate_age(year_of_birth):
    11    def calculate_age(self, year_of_birth):
    12        # return self.this_year - year_of_birth
    13        return (
    14            datetime.date.today().year
    15          - year_of_birth
    16        )
    17
    18    def test_joe(self):
    
  • I add the staticmethod decorator since calculate_age no longer uses anything from the TestPerson class

    10    # def calculate_age(year_of_birth):
    11    @staticmethod
    12    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'
    

    because a method of an instance takes the instance of the class (self) it belongs to as the first argument.

  • I remove self from the parentheses

    10    # def calculate_age(year_of_birth):
    11    @staticmethod
    12    # def calculate_age(self, year_of_birth):
    13    def calculate_age(year_of_birth):
    

    the test is green again.

  • I remove the commented lines and this_year attribute since it is no longer used

     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):
    

    this works because Python follows the following path when self.calculate_age(year_of_birth) is called

    self.calculate_age(year_of_birth)
    └── class TestPerson(unittest.TestCase)
           @staticmethod
        └── def calculate_age(year_of_birth):
            └── return (
                    datetime.date.today().year
                  - year_of_birth
                )
    

    when datetime.date.today() runs, I imagine Python follows this path

    datetime.date.today()
    datetime
        └── class date
            └── today()
                └── return self.date(YYYY, MM, DD)
    

    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 YYYY is 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 open person/__init__.py from the src folder

  • I add a function to calculate the age, with the same body as the calculate_age method, in src/person/__init__.py

    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        )
    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__.py

    1import datetime
    2
    3
    4class Person:
    

    all tests are green again.

  • I remove the commented line

    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 factory(
    42        first_name, last_name,
    43        sex, year_of_birth,
    44    ):
    

    still green.

  • I remove the commented line

    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 factory(
    
  • I change the calculation in the calculate_age method to make sure the tests work, in test_person.py

     8    @staticmethod
     9    def calculate_age(year_of_birth):
    10        return 1900 - year_of_birth
    11        return (
    12            datetime.date.today().year
    13          - year_of_birth
    14        )
    15
    16    def test_joe(self):
    
    • The terminal is my friend, and shows AssertionError for all four people.

    • The ages of the expectations are all negative numbers, 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

     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):
    

    green again.

  • I add a git commit message in the other terminal

    git commit -am \
    'add calculate_age function'
    

assert person is alive

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 calculate_age function in src/person/__init__.py

    23def 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       AssertionError
    

    another problem, the error message does not tell me much. At least the short test summary info shows me what test the error happened in

    FAILED ...::TestPerson::test_john - AssertionError
    

GREEN: make it pass


I change the value of year_of_birth in test_john in test_person.py

23    def test_john(self):
24        first_name = 'john'
25        last_name = 'smith'
26        sex = 'M'
27        # year_of_birth = 1580
28        year_of_birth = 1980

the test passes.


REFACTOR: make it better


  • I add a comment about the bad year_of_birth

    23    def test_john(self):
    24        first_name = 'john'
    25        last_name = 'smith'
    26        sex = 'M'
    27        year_of_birth = 1980
    28        # year_of_birth = 1580
    29        # raises AssertionError
    30        # because older than 120
    
  • I remove the commented line from the calculate_age function in src/person/__init__.py

    23def 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 'assert person is alive'
    

test_when_year_of_birth_is_not_an_integer

I want the Person class to make sure that the value for year_of_birth is an integer (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_birth is not an integer

    186        reality = mary.say_hello()
    187        assert reality == my_expectation
    188        self.assertEqual(reality, my_expectation)
    189
    190    def test_when_year_of_birth_is_not_an_integer(self):
    191        person = src.person.Person(
    192            first_name='first_name',
    193            last_name='last_name',
    194            sex='M',
    195        )
    196
    197    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_birth an optional argument in the Person class in src/person/__init__.py

     4class Person:
     5
     6    def __init__(
     7        self, first_name, last_name,
     8        # sex, year_of_birth,
     9        sex, year_of_birth=None,
    10    ):
    

    the terminal shows TypeError

    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 function only gets 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 <= 120
    31    return age
    32
    33
    34def say_hello(
    35    first_name, last_name, year_of_birth,
    36):
    

    the terminal is my friend, and shows AssertionError

    E       AssertionError
    

    the error message is still a problem. The short test summary info shows me what test the error happened in

    FAILED ...test_when_year_of_birth_is_not_an_integer - AssertionError
    
  • I add a comment, then change year_of_birth from the default value to a boolean in test_when_year_of_birth_is_not_an_integer, in test_person.py

    190    def test_when_year_of_birth_is_not_an_integer(self):
    191        person = src.person.Person(
    192            first_name='first_name',
    193            last_name='last_name',
    194            sex='M',
    195            # year_of_birth=None,    # fails
    196            year_of_birth=False,
    197        )
    198
    199    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 at assert isinstance(year_of_birth, int). This means a boolean is also an integer.

  • I change year_of_birth to a float

    190    def test_when_year_of_birth_is_not_an_integer(self):
    191        person = src.person.Person(
    192            first_name='first_name',
    193            last_name='last_name',
    194            sex='M',
    195            # year_of_birth=None,    # fails
    196            year_of_birth=2026.0,
    197        )
    198
    199    def test_dir_person_class(self):
    

    the terminal shows AssertionError

  • I add a comment then change year_of_birth to a string

    190    def test_when_year_of_birth_is_not_an_integer(self):
    191        person = src.person.Person(
    192            first_name='first_name',
    193            last_name='last_name',
    194            sex='M',
    195            # year_of_birth=None,    # fails
    196            # year_of_birth=2026.0,  # fails
    197            year_of_birth='2026',
    198        )
    199
    200    def test_dir_person_class(self):
    

    the terminal shows AssertionError

  • I add a comment then change year_of_birth to a tuple

    190    def test_when_year_of_birth_is_not_an_integer(self):
    191        person = src.person.Person(
    192            first_name='first_name',
    193            last_name='last_name',
    194            sex='M',
    195            # year_of_birth=None,    # fails
    196            # year_of_birth=2026.0,  # fails
    197            # year_of_birth='2026',  # fails
    198            year_of_birth=(2026,),
    199        )
    200
    201    def test_dir_person_class(self):
    
  • I add a comment

    190    def test_when_year_of_birth_is_not_an_integer(self):
    191        person = src.person.Person(
    192            first_name='first_name',
    193            last_name='last_name',
    194            sex='M',
    195            # year_of_birth=None,    # fails
    196            # year_of_birth=2026.0,  # fails
    197            # year_of_birth='2026',  # fails
    198            # year_of_birth=(2026,), # fails
    199        )
    200
    201    def test_dir_person_class(self):
    

    the test is green because there is no assertion or calls that cause AssertionError.

  • I remove the commented line from the Person class in src/person/__init__.py

    4class 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 test_person.py and src/person/__init__.py

  • I 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 person

    cd ..
    

    the terminal shows

    ...\pumping_python
    

    I am back in the pumping_python directory.


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

Do you want to see all the CODE I typed in this chapter?


what is next?

Would you like to test None (the simplest object)?


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.