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

person/tests/test_person.py
 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        ):
person/tests/test_person.py
27    def calculate_age(self, year_of_birth):
28        return self.this_year - year_of_birth
person/tests/test_person.py
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        )
person/tests/test_person.py
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        )
person/tests/test_person.py
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        )
person/tests/test_person.py
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
person/tests/test_person.py
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
person/tests/test_person.py
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 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 tests/test_person.py

    180    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 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 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 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 tests/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.


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 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 seen
    
  • I copy (ctrl/command+c) the values from the terminal, paste (ctrl/command+v) them as my_expectation and remove the extra characters

    232    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 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 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.year object is

    257    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 seen
    

    the 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 seen
    

    the 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.today method to see what it returns

    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            datetime.date.today(),
    265            []
    266        )
    267
    268
    269# 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 to datetime.date.today() is datetime.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 attributes

    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            # datetime.date.today(),
    265            dir(datetime.date.today()),
    266            []
    267        )
    268
    269
    270# 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

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

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().year in the assert_say_hello_works method

    27    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().year in the assert_person_can_say_hello method

    46    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_birth in the assert_say_hello_works method

    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' {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_birth in 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' {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 self from the parentheses

    27    @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) runs

    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
            │   )
            │
            └── 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 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 __init__.py from the person folder in 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 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.py

    25    @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__.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 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 120

    145def 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       AssertionError
    
    • I use datetime.date.today().year - 121 to make sure the person is always older than 120

    • The short test summary info shows me what test the error happened in

      FAILED ...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__.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 \
    '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_birth is in the future, in tests/test_person.py

    178        # ).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 + 1 for 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, 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 >= 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       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_the_future - AssertionError
    
  • I remove the assertion and call to the say_hello method in test_when_year_of_birth_is_the_future, in tests/test_person.py

    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        )
    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().year in the calculate_age method

    27    @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().year in 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=datetime.date.today().year-121
    175            year_of_birth=self.this_year-121
    176        )
    177        # ).say_hello() fails
    178        # because age > 120
    

    the 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().year in 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=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_birth is not an integer

    186        # ).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_birth an optional argument in the constructor method of 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 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       AssertionError
    

    another bad error message. 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 add False as the value for if a boolean is given as the value for the year_of_birth parameter in test_when_year_of_birth_is_not_an_integer, in tests/test_person.py

    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=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 at assert isinstance(year_of_birth, int). This means a boolean is also an integer.

  • I change year_of_birth from False to a float

    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,
    196        ).say_hello()
    197
    198    def test_dir_person_class(self):
    

    the terminal shows AssertionError

  • I add a comment then change year_of_birth to a string

    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',
    197        ).say_hello()
    198
    199    def test_dir_person_class(self):
    

    the terminal shows AssertionError

  • I add a comment then change year_of_birth to a tuple

    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,),
    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__.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 tests/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


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.