classes

I made functions that make dictionaries and strings in how to make a person. I can also do the same thing with a class since it is a group of attributes (variables) and methods (functions) that belong together.


preview

I have these tests by the end of the chapter

  1import datetime
  2import random
  3import src.person
  4import unittest
  5
  6
  7def pick_one(*choices):
  8    return random.choice(choices)
  9
 10
 11def get_random_name():
 12    return pick_one(
 13        'jane', 'joe', 'john', 'person',
 14        'doe', 'smith', 'blow', 'public',
 15    )
 16
 17
 18class TestPerson(unittest.TestCase):
 19
 20    def setUp(self):
 21        self.random_first_name = get_random_name()
 22        self.random_last_name = get_random_name()
 23        this_year = datetime.datetime.now().year
 24        self.random_year_of_birth = random.randint(
 25            this_year-120, this_year
 26        )
 27        self.age = this_year - self.random_year_of_birth
 28
 29    def test_factory_w_keyword_arguments(self):
 30        a_person = dict(
 31            first_name=self.random_first_name,
 32            last_name=self.random_last_name,
 33            sex=pick_one('F', 'M'),
 34        )
 35
 36        reality = src.person.factory(
 37            **a_person,
 38            year_of_birth=self.random_year_of_birth,
 39        )
 40        my_expectation = dict(
 41            **a_person,
 42            age=self.age,
 43        )
 44        self.assertEqual(reality, my_expectation)
 45
 46    def test_factory_w_optional_arguments(self):
 47        reality = src.person.factory(
 48            first_name=self.random_first_name,
 49            year_of_birth=self.random_year_of_birth,
 50        )
 51        my_expectation = dict(
 52            first_name=self.random_first_name,
 53            last_name='doe',
 54            sex='M',
 55            age=self.age,
 56        )
 57        self.assertEqual(reality, my_expectation)
 58
 59    def test_factory_person_says_hello(self):
 60        a_random_person = src.person.factory(
 61            first_name=self.random_first_name,
 62            last_name=self.random_last_name,
 63            year_of_birth=self.random_year_of_birth,
 64        )
 65
 66        reality = src.person.say_hello(a_random_person)
 67        my_expectation = (
 68            f'Hi, my name is {self.random_first_name}'
 69            f' {self.random_last_name}'
 70            f' and I am {self.age}'
 71        )
 72        self.assertEqual(reality, my_expectation)
 73
 74    def test_classy_person_says_hello(self):
 75        a_random_person = src.person.Person(
 76            first_name=self.random_first_name,
 77            last_name=self.random_last_name,
 78            year_of_birth=self.random_year_of_birth,
 79        )
 80
 81        reality = a_random_person.say_hello()
 82        my_expectation = (
 83            f'Hi, my name is {self.random_first_name}'
 84            f' {self.random_last_name}'
 85            f' and I am {self.age}'
 86        )
 87        self.assertEqual(reality, my_expectation)
 88
 89    def test_attributes_and_methods_of_person_class(self):
 90        reality = dir(src.person.Person)
 91        my_expectation = [
 92            '__class__',
 93            '__delattr__',
 94            '__dict__',
 95            '__dir__',
 96            '__doc__',
 97            '__eq__',
 98            '__firstlineno__',
 99            '__format__',
100            '__ge__',
101            '__getattribute__',
102            '__getstate__',
103            '__gt__',
104            '__hash__',
105            '__init__',
106            '__init_subclass__',
107            '__le__',
108            '__lt__',
109            '__module__',
110            '__ne__',
111            '__new__',
112            '__reduce__',
113            '__reduce_ex__',
114            '__repr__',
115            '__setattr__',
116            '__sizeof__',
117            '__static_attributes__',
118            '__str__',
119            '__subclasshook__',
120            '__weakref__',
121            'say_hello',
122        ]
123        self.assertEqual(reality, my_expectation)
124
125    def test_attributes_and_methods_of_person_instance(self):
126        an_instance_of_person = src.person.Person(
127            first_name=self.random_first_name,
128            last_name=self.random_last_name,
129            year_of_birth=self.random_year_of_birth,
130            sex=pick_one('F', 'M')
131        )
132
133        reality = dir(an_instance_of_person)
134        my_expectation = [
135            '__class__',
136            '__delattr__',
137            '__dict__',
138            '__dir__',
139            '__doc__',
140            '__eq__',
141            '__firstlineno__',
142            '__format__',
143            '__ge__',
144            '__getattribute__',
145            '__getstate__',
146            '__gt__',
147            '__hash__',
148            '__init__',
149            '__init_subclass__',
150            '__le__',
151            '__lt__',
152            '__module__',
153            '__ne__',
154            '__new__',
155            '__reduce__',
156            '__reduce_ex__',
157            '__repr__',
158            '__setattr__',
159            '__sizeof__',
160            '__static_attributes__',
161            '__str__',
162            '__subclasshook__',
163            '__weakref__',
164            'first_name',
165            'last_name',
166            'say_hello',
167            'sex',
168            'year_of_birth',
169        ]
170        self.assertEqual(reality, my_expectation)
171
172
173# Exceptions seen
174# AssertionError
175# NameError
176# AttributeError
177# TypeError
178# SyntaxError

questions about classes

Questions to think about as I go through the chapter


requirements

how to make a person


open the project

  • I change directory to the person folder

    cd person
    

    the terminal shows I am in the person folder

    .../pumping_python/person
    
  • I use pytest-watcher to run the tests

    uv run pytest-watcher . --now
    

    the terminal is my friend, and shows

    rootdir: .../pumping_python/person
    configfile: pyproject.toml
    collected 3 items
    
    tests/test_person.py ...                              [100%]
    
    ==================== 3 passed in X.YZs =====================
    
  • I hold ctrl on the keyboard, then click on tests/test_person.py to open it


test_classy_person_says_hello

I made a person say hello with a function, I can also do the same thing with a class because it is attributes and methods that belong together.


RED: make it fail


I add a new test to test_person.py

 69    def test_factory_person_says_hello(self):
 70        first_name = get_random_name()
 71        last_name = get_random_name()
 72        sex = pick_one('F', 'M')
 73
 74        year_of_birth = get_random_year_of_birth()
 75        age = calculate_age(year_of_birth)
 76
 77        a_random_person = src.person.factory(
 78            first_name=first_name,
 79            last_name=last_name,
 80            sex=sex,
 81            year_of_birth=year_of_birth,
 82        )
 83
 84        reality = src.person.say_hello(a_random_person)
 85        my_expectation = (
 86            f'Hi, my name is {first_name} {last_name}'
 87            f' and I am {age}'
 88        )
 89        self.assertEqual(reality, my_expectation)
 90
 91    def test_classy_person_says_hello(self):
 92        joe = src.person.Person(
 93            first_name='joe',
 94            last_name='blow',
 95            year_of_birth=1996,
 96        )
 97
 98        reality = src.person.say_hello(joe)
 99        my_expectation = None
100        self.assertEqual(reality, my_expectation)
101
102
103# Exceptions seen

the terminal is my friend, and shows AttributeError

AttributeError: module 'src.person' has no attribute 'Person'

because there is no definition for Person in person.py in the src folder


GREEN: make it pass


  • I add a class to person.py

    12def factory(
    13        first_name, year_of_birth,
    14        last_name='doe', sex='M',
    15    ):
    16    return {
    17        'first_name': first_name,
    18        'last_name': last_name,
    19        'sex': sex,
    20        'age': (
    21            datetime.datetime.today().year
    22          - year_of_birth
    23        ),
    24    }
    25
    26
    27class Person:
    28
    29    pass
    
  • I add a constructor method to the Person class so it can take arguments, it is used to define how copies of the class are made

    27class Person:
    28
    29    # pass
    30    def __init__():
    31        return None
    

    the terminal is my friend, and shows TypeError

    TypeError:
        Person.__init__() got an
        unexpected keyword argument 'first_name'
    
    • because the definition for __init__ does not allow calling it with inputs (the parentheses are empty) and the test sends 'first_name' as input.

    • a constructor method is used to make copies of a class

  • I add the name in parentheses so that the __init__ constructor method can take input

    27class Person:
    28
    29    # pass
    30    # def __init__():
    31    def __init__(first_name):
    32        return None
    

    the terminal is my friend, and shows TypeError

    TypeError:
        Person.__init__() got
        multiple values for argument 'first_name'
    

    because the __init__ constructor method takes the instance it belongs to as the first argument

  • I add self as the first argument the way I do with all the test methods in the book

    27class Person:
    28
    29    # pass
    30    # def __init__():
    31    # def __init__(first_name):
    32    def __init__(self, first_name):
    33        return None
    
    • self is Python convention, I can use any name I want

    • the terminal is my friend, and shows TypeError

      TypeError:
          Person.__init__() got
          an unexpected keyword argument 'last_name'.
          Did you mean 'first_name'?
      

      I have seen this before, so far it is the same as making the factory function

  • I add last_name to the definition of __init__

    27class Person:
    28
    29    # pass
    30    # def __init__():
    31    # def __init__(first_name):
    32    # def __init__(self, first_name):
    33    def __init__(self, first_name, last_name):
    34        return None
    

    the terminal is my friend, and shows TypeError

    TypeError:
        Person.__init__() got
        an unexpected keyword argument 'year_of_birth'
    

    still the same as making the factory function

  • I add year_of_birth to the definition of the __init__ method

    27class Person:
    28
    29    # pass
    30    # def __init__():
    31    # def __init__(first_name):
    32    # def __init__(self, first_name):
    33    # def __init__(self, first_name, last_name):
    34    def __init__(
    35        self, first_name, last_name,
    36        year_of_birth,
    37    ):
    38        return None
    

    the terminal is my friend, and shows AttributeError

    AttributeError:
        'Person' object has no attribute 'get'
    

    because

  • I change reality in test_classy_person_says_hello to use a method I can add to Person, in test_person.py

     91    def test_classy_person_says_hello(self):
     92        joe = src.person.Person(
     93            first_name='joe',
     94            last_name='blow',
     95            year_of_birth=1996,
     96        )
     97
     98        # reality = src.person.say_hello(joe)
     99        reality = src.person.Person.say_hello(joe)
    100        my_expectation = None
    101        self.assertEqual(reality, my_expectation)
    102
    103
    104# Exceptions seen
    

    the terminal is my friend, and shows AttributeError

    AttributeError:
        'Person' object has no attribute 'say_hello'
    

    because the test calls the say_hello function which does not yet exist in the Person class

  • I add a method definition for it to the Person class in person.py

    27class Person:
    28
    29    # pass
    30    # def __init__():
    31    # def __init__(first_name):
    32    # def __init__(self, first_name):
    33    # def __init__(self, first_name, last_name):
    34    def __init__(
    35        self, first_name, last_name,
    36        year_of_birth,
    37    ):
    38        return None
    39
    40    def say_hello():
    41        return None
    

    the terminal is my friend, and shows TypeError

    TypeError:
        Person.say_hello() takes 0 positional arguments
        but 2 were given
    

    because the definition for say_hello does not allow inputs and the test called the method with one positional argument (person). Why did the error say two were given when the test only sends one?

  • I add person to the method definition

    27class Person:
    28
    29    # pass
    30    # def __init__():
    31    # def __init__(first_name):
    32    # def __init__(self, first_name):
    33    # def __init__(self, first_name, last_name):
    34    def __init__(
    35        self, first_name, last_name,
    36        year_of_birth,
    37    ):
    38        return None
    39
    40    # def say_hello():
    41    def say_hello(person):
    42        return None
    

    the terminal is my friend, and shows TypeError

    TypeError:
        Person.say_hello() takes 1 positional argument
        but 2 were given
    

    because methods take the copy of the class (self) they belong to as the first argument.


what is the staticmethod decorator?


  • I can use the staticmethod decorator if I do not want to add self to the method definition when it does not use anything in the class that way I am not sending more information than what the method needs. I add @staticmethod to say_hello

    27class Person:
    28
    29    # pass
    30    # def __init__():
    31    # def __init__(first_name):
    32    # def __init__(self, first_name):
    33    # def __init__(self, first_name, last_name):
    34    def __init__(
    35        self, first_name, last_name,
    36        year_of_birth,
    37    ):
    38        return None
    39
    40    # def say_hello():
    41    @staticmethod
    42    def say_hello(person):
    43        return None
    

    the test passes. I can call methods from outside the class they belong to.

    • I made a copy of the Person class named joe

    • I called the say_hello method of the Person class with joe (which is a copy of the Person class) as input. Confused? It is confusing and there is a better way.


REFACTOR: make it better


I want the say_hello method of the Person class to return a string for the person it receives, the same way the say_hello function returns a string for the person (dictionary) it receives as input

  • I change my_expectation to an f-string in test_classy_person_says_hello in test_person.py

     91    def test_classy_person_says_hello(self):
     92        joe = src.person.Person(
     93            first_name='joe',
     94            last_name='blow',
     95            year_of_birth=1996,
     96        )
     97
     98        # reality = src.person.say_hello(joe)
     99        reality = src.person.Person.say_hello(joe)
    100        # my_expectation = None
    101        my_expectation = (
    102            'Hi, my name is joe blow and I am'
    103            f' {calculate_age(1996)}'
    104        )
    105        self.assertEqual(reality, my_expectation)
    106
    107
    108# Exceptions seen
    

    the terminal is my friend, and shows AssertionError

    AssertionError: None != 'Hi, my name is joe blow and I am 30'
    
  • I copy the value from the terminal and paste it in the return statement for the say_hello method of the Person class in person.py

    40    # def say_hello():
    41    @staticmethod
    42    def say_hello(person):
    43        # return None
    44        return 'Hi, my name is joe blow and I am 30'
    

    the test passes.

  • I add an assertion for the next person to test_classy_person_says_hello in test_person.py

     98        # reality = src.person.say_hello(joe)
     99        reality = src.person.Person.say_hello(joe)
    100        # my_expectation = None
    101        my_expectation = (
    102            'Hi, my name is joe blow and I am'
    103            f' {calculate_age(1996)}'
    104        )
    105        self.assertEqual(reality, my_expectation)
    106
    107        jane = src.person.Person(
    108            first_name='jane',
    109            sex='F',
    110            year_of_birth=1991,
    111        )
    112
    113        reality = src.person.Person.say_hello(jane)
    114        my_expectation = (
    115            'Hi, my name is jane doe and I am'
    116            f' {calculate_age(1991)}'
    117        )
    118        self.assertEqual(reality, my_expectation)
    119
    120
    121# Exceptions seen
    

    the terminal is my friend, and shows TypeError

    TypeError:
        Person.__init__() got an
        unexpected keyword argument 'sex'
    

    because the __init__ method definition does not have a parameter named sex

  • I add sex to the definition of the __init__ method

    27class Person:
    28
    29    # pass
    30    # def __init__():
    31    # def __init__(first_name):
    32    # def __init__(self, first_name):
    33    # def __init__(self, first_name, last_name):
    34    def __init__(
    35        self, first_name, last_name,
    36        # year_of_birth,
    37        year_of_birth, sex,
    38    ):
    39        return None
    

    the terminal is my friend, and shows TypeError

    TypeError:
        Person.__init__() missing
        1 required positional argument: 'sex'
    

    because when the test calls the Person object to make joe it does not provide a value for sex which I just made a required argument when I added it to the __init__ method definition, I have to make it a choice

  • I add a default value for sex to make it optional

    34    def __init__(
    35        self, first_name, last_name,
    36        # year_of_birth,
    37        # year_of_birth, sex,
    38        year_of_birth, sex=None,
    39    ):
    40        return None
    

    the terminal is my friend, and shows TypeError

    TypeError:
        Person.__init__() missing
        1 required positional argument: 'last_name'
    

    because when the test calls the Person object to make jane it does not provide a value for last_name which is a required argument, I have to make it a choice as well

  • I add a default value for last_name to make it optional

    34    def __init__(
    35        # self, first_name, last_name,
    36        self, first_name, last_name=None,
    37        # year_of_birth,
    38        # year_of_birth, sex,
    39        year_of_birth, sex=None,
    40    ):
    41        return None
    

    the terminal is my friend, and shows SyntaxError

    SyntaxError: parameter without a default follows
                 parameter with a default
    

    because parameters without default values must come before parameters with default values

  • I add a default value for year_of_birth to make it optional

    34    def __init__(
    35        # self, first_name, last_name,
    36        self, first_name, last_name=None,
    37        # year_of_birth,
    38        # year_of_birth, sex,
    39        # year_of_birth, sex=None,
    40        year_of_birth=None, sex=None,
    41    ):
    42        return None
    

    the terminal is my friend, and shows AssertionError

    AssertionError:
          'Hi, my name is joe blow and I am 30'
       != 'Hi, my name is jane doe and I am 35'
    

    Progress. I can make the say_hello function use attributes of the person it receives as input to make the message.

  • I change the string in the return statement of the say_hello method of the Person class to an f-string with the first_name attribute of the person it receives, in person.py

    44    # def say_hello():
    45    @staticmethod
    46    def say_hello(person):
    47        # return None
    48        # return 'Hi, my name is joe blow and I am 30'
    49        return (
    50            f'Hi, my name is {person.first_name} blow'
    51            ' and I am 30'
    52        )
    

    the terminal is my friend, and shows AttributeError

    AttributeError:
        'Person' object has no attribute 'first_name'
    

    because there is no definition for first_name in the Person class definition

  • I add an attribute to the Person class for first_name

    27class Person:
    28
    29    first_name = 'jane'
    30
    31    # pass
    

    the terminal is my friend, and shows AssertionError

    AssertionError:
        'Hi, my name is jane blow and I am 30'
     != 'Hi, my name is joe blow and I am 30'
    

    because I used a fixed value (jane) and the first assertion of the test expects joe. I have to get the value from the object that is passed to the say_hello method.

  • I add a variable to the __init__ method to use it to allow changing the first_name attribute anytime a copy of the Person class is made

    27class Person:
    28
    29    first_name = 'jane'
    30
    31    # pass
    32    # def __init__():
    33    # def __init__(first_name):
    34    # def __init__(self, first_name):
    35    # def __init__(self, first_name, last_name):
    36    def __init__(
    37        # self, first_name, last_name,
    38        self, first_name, last_name=None,
    39        # year_of_birth,
    40        # year_of_birth, sex,
    41        # year_of_birth, sex=None,
    42        year_of_birth=None, sex=None,
    43    ):
    44        first_name = first_name
    45        return None
    

    the terminal still shows AssertionError

    AssertionError:
        'Hi, my name is jane blow and I am 30'
     != 'Hi, my name is joe blow and I am 30'
    
  • I change the variable to a class attribute by adding self. before it

    36    def __init__(
    37        # self, first_name, last_name,
    38        self, first_name, last_name=None,
    39        # year_of_birth,
    40        # year_of_birth, sex,
    41        # year_of_birth, sex=None,
    42        year_of_birth=None, sex=None,
    43    ):
    44        # first_name = first_name
    45        self.first_name = first_name
    46        return None
    

    the terminal is my friend, and shows AssertionError

    AssertionError:
        'Hi, my name is jane blow and I am 30'
     != 'Hi, my name is jane doe and I am 35'
    

    the first names are the same, the last names and ages are different


  • I add the last_name attribute to the string in the return statement of the say_hello method

    48    # def say_hello():
    49    @staticmethod
    50    def say_hello(person):
    51        # return None
    52        # return 'Hi, my name is joe blow and I am 30'
    53        return (
    54            # f'Hi, my name is {person.first_name} blow'
    55            f'Hi, my name is {person.first_name}'
    56            f' {person.last_name}'
    57            ' and I am 30'
    58        )
    

    the terminal is my friend, and shows AttributeError

    AttributeError:
        'Person' object has no attribute 'last_name'.
        Did you mean: 'first_name'?
    

    because there is no definition for last_name in the Person class definition

  • I add an attribute to the Person class for last_name

    27class Person:
    28
    29    first_name = 'jane'
    30    last_name = 'doe'
    31
    32    # pass
    

    the terminal is my friend, and shows AssertionError

    AssertionError:
        'Hi, my name is joe doe and I am 30'
     != 'Hi, my name is joe blow and I am 30'
    

    because I used a fixed value (doe) and the first assertion of the test expects blow. I have to get the value from the object that is passed to the say_hello method.

  • I add a variable to the __init__ method to use it to allow changing the last_name attribute anytime a copy of the Person class is made

    32    # pass
    33    # def __init__():
    34    # def __init__(first_name):
    35    # def __init__(self, first_name):
    36    # def __init__(self, first_name, last_name):
    37    def __init__(
    38        # self, first_name, last_name,
    39        self, first_name, last_name=None,
    40        # year_of_birth,
    41        # year_of_birth, sex,
    42        # year_of_birth, sex=None,
    43        year_of_birth=None, sex=None,
    44    ):
    45        # first_name = first_name
    46        self.first_name = first_name
    47        last_name = last_name
    48        return None
    

    the terminal still shows AssertionError

    AssertionError:
        'Hi, my name is joe doe and I am 30'
     != 'Hi, my name is joe blow and I am 30'
    
  • I change last_name to a class attribute in the __init__ method by adding self. before it

    37    def __init__(
    38        # self, first_name, last_name,
    39        self, first_name, last_name=None,
    40        # year_of_birth,
    41        # year_of_birth, sex,
    42        # year_of_birth, sex=None,
    43        year_of_birth=None, sex=None,
    44    ):
    45        # first_name = first_name
    46        self.first_name = first_name
    47        # last_name = last_name
    48        self.last_name = last_name
    49        return None
    

    the terminal is my friend, and shows AssertionError

    AssertionError:
        'Hi, my name is jane None and I am 30'
     != 'Hi, my name is jane doe and I am 35'
    
  • I change the default value for last_name in the __init__ method to 'doe' to give the test what it wants

    37    def __init__(
    38        # self, first_name, last_name,
    39        # self, first_name, last_name=None,
    40        self, first_name, last_name='doe',
    41        # year_of_birth,
    42        # year_of_birth, sex,
    43        # year_of_birth, sex=None,
    44        year_of_birth=None, sex=None,
    45    ):
    

    the terminal is my friend, and shows AssertionError

    AssertionError:
        'Hi, my name is jane doe and I am 30'
     != 'Hi, my name is jane doe and I am 35'
    

    the age is the only thing that is different


  • I add a calculation for the age with the year_of_birth attribute to the return statement of the say_hello method

    52    # def say_hello():
    53    @staticmethod
    54    def say_hello(person):
    55        age = (
    56            datetime.datetime.today().year
    57          - person.year_of_birth
    58        )
    59        # return None
    60        # return 'Hi, my name is joe blow and I am 30'
    61        return (
    62            # f'Hi, my name is {person.first_name} blow'
    63            f'Hi, my name is {person.first_name}'
    64            f' {person.last_name}'
    65            # f' and I am 30'
    66            f' and I am {age}'
    67        )
    

    the terminal is my friend, and shows AttributeError

    AttributeError:
        'Person' object has no attribute 'year_of_birth'
    

    because there is no definition for year_of_birth in the Person class definition

  • I add an attribute to the Person class for year_of_birth

    27class Person:
    28
    29    first_name = 'jane'
    30    last_name = 'doe'
    31    year_of_birth = 1991
    32
    33    # pass
    

    the terminal is my friend, and shows AssertionError

    AssertionError:
        'Hi, my name is joe blow and I am 35'
     != 'Hi, my name is joe blow and I am 30'
    

    because I used a fixed value (1991) and the first assertion of the test expects datetime.datetime.now().year-1996. I have to get the value from the object that is passed to the say_hello method.

  • I add a variable to the __init__ method to use it to allow changing the year_of_birth attribute anytime a copy of the Person class is made

    38    def __init__(
    39        # self, first_name, last_name,
    40        # self, first_name, last_name=None,
    41        self, first_name, last_name='doe',
    42        # year_of_birth,
    43        # year_of_birth, sex,
    44        # year_of_birth, sex=None,
    45        year_of_birth=None, sex=None,
    46    ):
    47        # first_name = first_name
    48        self.first_name = first_name
    49        # last_name = last_name
    50        self.last_name = last_name
    51        year_of_birth = year_of_birth
    52        return None
    

    the terminal still shows AssertionError

    AssertionError:
        'Hi, my name is joe blow and I am 35'
     != 'Hi, my name is joe blow and I am 30'
    
  • I change year_of_birth to a class attribute in the __init__ method by adding self. before it

    38    def __init__(
    39        # self, first_name, last_name,
    40        # self, first_name, last_name=None,
    41        self, first_name, last_name='doe',
    42        # year_of_birth,
    43        # year_of_birth, sex,
    44        # year_of_birth, sex=None,
    45        year_of_birth=None, sex=None,
    46    ):
    47        # first_name = first_name
    48        self.first_name = first_name
    49        # last_name = last_name
    50        self.last_name = last_name
    51        # year_of_birth = year_of_birth
    52        self.year_of_birth = year_of_birth
    53        return None
    

    the test passes. What a beautiful life.

  • self.first_name, self.last_name and self.year_of_birth are now defined twice in the class. I remove the first definition since the attributes are also made in the __init__ method and that gets called when copies of the Person class are made, no need to have a default person be jane doe born in 1991

    27class Person:
    28
    29    # first_name = 'jane'
    30    # last_name = 'doe'
    31    # year_of_birth = 1991
    32
    33    # pass
    

    the test is still green.

  • datetime.datetime.today().year gets used to calculate the age in the say_hello method of the Person class and the return statement of the factory function. I make a helper function to calculate the age, the same way I do in the tests

     1import datetime
     2
     3
     4def calculate_age(year_of_birth):
     5    return (
     6        datetime.datetime.today().year
     7      - year_of_birth
     8    )
     9
    10
    11def say_hello(a_dictionary):
    
  • I use the new function for the age calculation in the factory function

    19def factory(
    20        first_name, year_of_birth,
    21        last_name='doe', sex='M',
    22    ):
    23    return {
    24        'first_name': first_name,
    25        'last_name': last_name,
    26        'sex': sex,
    27        # 'age': (
    28        #     datetime.datetime.today().year
    29        #   - year_of_birth
    30        # ),
    31        'age': calculate_age(year_of_birth),
    32    }
    33
    34
    35class Person:
    

    still green.

  • I use the new function for the age calculation in the say_hello method of the Person class

    63    # def say_hello():
    64    @staticmethod
    65    def say_hello(person):
    66        # age = (
    67        #     datetime.datetime.today().year
    68        #   - person.year_of_birth
    69        # )
    70        age = calculate_age(person.year_of_birth)
    71        # return None
    72        # return 'Hi, my name is joe blow and I am 30'
    73        return (
    74            # f'Hi, my name is {person.first_name} blow'
    75            f'Hi, my name is {person.first_name}'
    76            f' {person.last_name}'
    77            # f' and I am 30'
    78            f' and I am {age}'
    79        )
    

    green.

  • The say_hello method is in the Person class, there is no need for it to take a copy of the Person class as input since it should be able to access the attributes of the class it belongs to. I change person. to self. to use class attributes instead

    63    # def say_hello():
    64    @staticmethod
    65    def say_hello(person):
    66        # age = (
    67        #     datetime.datetime.today().year
    68        #   - person.year_of_birth
    69        # )
    70        # age = calculate_age(person.year_of_birth)
    71        age = calculate_age(self.year_of_birth)
    72        # return None
    73        # return 'Hi, my name is joe blow and I am 30'
    74        return (
    75            # f'Hi, my name is {person.first_name} blow'
    76            # f'Hi, my name is {person.first_name}'
    77            # f' {person.last_name}'
    78            # f' and I am 30'
    79            f'Hi, my name is {self.first_name}'
    80            f' {self.last_name}'
    81            f' and I am {age}'
    82        )
    

    the terminal is my friend, and shows NameError

    NameError: name 'self' is not defined
    
  • I change the name of the input parameter from person to self

    63    # def say_hello():
    64    @staticmethod
    65    # def say_hello(person):
    66    def say_hello(self):
    

    the test is green again.

  • I remove the staticmethod decorator because I no longer need it since the say_hello method is using class attributes

    63    # def say_hello():
    64    # @staticmethod
    65    # def say_hello(person):
    66    def say_hello(self):
    

    the test is still green.

  • I change the call to src.person.say_hello(joe) for joe because I can call methods directly from a copy of a class, in test_classy_person_says_hello in test_person.py

     98        # reality = src.person.say_hello(joe)
     99        # reality = src.person.Person.say_hello(joe)
    100        reality = joe.say_hello()
    101        # my_expectation = None
    102        my_expectation = (
    103            'Hi, my name is joe blow and I am'
    104            f' {calculate_age(1996)}'
    105        )
    106        self.assertEqual(reality, my_expectation)
    107
    108        jane = src.person.Person(
    109            first_name='jane',
    110            sex='F',
    111            year_of_birth=1991,
    112        )
    

    still green.

  • I change the call to src.person.say_hello(joe) for jane as well

    108        jane = src.person.Person(
    109            first_name='jane',
    110            sex='F',
    111            year_of_birth=1991,
    112        )
    113
    114        # reality = src.person.Person.say_hello(jane)
    115        reality = jane.say_hello()
    116        my_expectation = (
    117            'Hi, my name is jane doe and I am'
    118            f' {calculate_age(1991)}'
    119        )
    120        self.assertEqual(reality, my_expectation)
    121
    122
    123# Exceptions seen
    

    green.

  • I add an assertion for the next person

    114        # reality = src.person.Person.say_hello(jane)
    115        reality = jane.say_hello()
    116        my_expectation = (
    117            'Hi, my name is jane doe and I am'
    118            f' {calculate_age(1991)}'
    119        )
    120        self.assertEqual(reality, my_expectation)
    121
    122        john = src.person.Person(
    123            first_name='john',
    124            last_name='smith',
    125            year_of_birth=1580,
    126        )
    127
    128        reality = john.say_hello()
    129        my_expectation = (
    130            'Hi, my name is jane doe and I am'
    131            f' {calculate_age(1991)}'
    132        )
    133        self.assertEqual(reality, my_expectation)
    134
    135
    136# Exceptions seen
    

    the terminal is my friend, and shows AssertionError

    AssertionError:
        'Hi, my name is john smith and I am 446'
     != 'Hi, my name is jane doe and I am 35'
    
  • I change my_expectation to match reality for john

    128        reality = john.say_hello()
    129        my_expectation = (
    130            # 'Hi, my name is jane doe and I am'
    131            # f' {calculate_age(1991)}'
    132            'Hi, my name is john smith and I am'
    133            f' {calculate_age(1580)}'
    134        )
    135        self.assertEqual(reality, my_expectation)
    

    the test passes.

  • I add an assertion for a_person

    128        reality = john.say_hello()
    129        my_expectation = (
    130            # 'Hi, my name is jane doe and I am'
    131            # f' {calculate_age(1991)}'
    132            'Hi, my name is john smith and I am'
    133            f' {calculate_age(1580)}'
    134        )
    135        self.assertEqual(reality, my_expectation)
    136
    137        a_person = src.person.Person(
    138            first_name='person',
    139            last_name='public',
    140            year_of_birth=2000,
    141            sex='F',
    142        )
    143
    144        reality = a_person.say_hello()
    145        my_expectation = (
    146            'Hi, my name is john smith and I am'
    147            f' {calculate_age(1580)}'
    148        )
    149        self.assertEqual(reality, my_expectation)
    150
    151
    152# Exceptions seen
    

    the terminal is my friend, and shows AssertionError

    AssertionError:
        'Hi, my name is person public and I am 26'
     != 'Hi, my name is john smith and I am 446'
    
  • I change my_expectation to match reality for a_person

    129        a_person = src.person.Person(
    130            first_name='person',
    131            last_name='public',
    132            year_of_birth=2000,
    133            sex='F',
    134        )
    135
    136        reality = a_person.say_hello()
    137        my_expectation = (
    138            'Hi, my name is person public and I am'
    139            f' {calculate_age(2000)}'
    140        )
    141        self.assertEqual(reality, my_expectation)
    142
    143
    144# Exceptions seen
    

    the test passes.

  • I open a new terminal then change directories to person

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

    git commit -am 'add test_classy_person_says_hello'
    

test_classy_person_says_hello with random values

I want to use random values to test_classy_person_says_hello

  • I go back to the terminal that is running the tests

  • I add variables

     91    def test_classy_person_says_hello(self):
     92        first_name = 'joe'
     93        last_name = 'blow'
     94
     95        year_of_birth = 1996
     96        age = calculate_age(year_of_birth)
     97
     98        joe = src.person.Person(
     99            first_name='joe',
    100            last_name='blow',
    101            year_of_birth=1996,
    102        )
    
  • I use the variables to remove repetition of 'joe', 'blow' and 1996

     96        joe = src.person.Person(
     97            # first_name='joe',
     98            # last_name='blow',
     99            # year_of_birth=1996,
    100            first_name=first_name,
    101            last_name=last_name,
    102            year_of_birth=year_of_birth,
    103        )
    104
    105        # reality = src.person.say_hello(joe)
    106        # reality = src.person.Person.say_hello(joe)
    107        reality = joe.say_hello()
    108        # my_expectation = None
    109        my_expectation = (
    110            # 'Hi, my name is joe blow and I am'
    111            # f' {calculate_age(1996)}'
    112            f'Hi, my name is {first_name} {last_name}'
    113            f' and I am {age}'
    114        )
    115        self.assertEqual(reality, my_expectation)
    

    the test is still green.

  • I add variables

    108        # reality = src.person.say_hello(joe)
    109        # reality = src.person.Person.say_hello(joe)
    110        reality = joe.say_hello()
    111        # my_expectation = None
    112        my_expectation = (
    113            # 'Hi, my name is joe blow and I am'
    114            # f' {calculate_age(1996)}'
    115            f'Hi, my name is {first_name} {last_name}'
    116            f' and I am {age}'
    117        )
    118        self.assertEqual(reality, my_expectation)
    119
    120        first_name = 'jane'
    121        last_name = 'doe'
    122
    123        year_of_birth = 1991
    124        age = calculate_age(year_of_birth)
    125
    126        jane = src.person.Person(
    127            first_name='jane',
    128            sex='F',
    129            year_of_birth=1991,
    130        )
    
  • I use the variables to remove repetition of 'jane' and 1991

    126        jane = src.person.Person(
    127            # first_name='jane',
    128            sex='F',
    129            # year_of_birth=1991,
    130            first_name=first_name,
    131            year_of_birth=year_of_birth,
    132        )
    133
    134        # reality = src.person.Person.say_hello(jane)
    135        reality = jane.say_hello()
    136        my_expectation = (
    137            # 'Hi, my name is jane doe and I am'
    138            # f' {calculate_age(1991)}'
    139            f'Hi, my name is {first_name} {last_name}'
    140            f' and I am {age}'
    141        )
    142        self.assertEqual(reality, my_expectation)
    143
    144        john = src.person.Person(
    145            first_name='john',
    146            last_name='smith',
    147            year_of_birth=1580,
    148        )
    

    the test is still green.

  • I do the same thing with john

    134        # reality = src.person.Person.say_hello(jane)
    135        reality = jane.say_hello()
    136        my_expectation = (
    137            # 'Hi, my name is jane doe and I am'
    138            # f' {calculate_age(1991)}'
    139            f'Hi, my name is {first_name} {last_name}'
    140            f' and I am {age}'
    141        )
    142        self.assertEqual(reality, my_expectation)
    143
    144        first_name = 'john'
    145        last_name = 'smith'
    146
    147        year_of_birth = 1580
    148        age = calculate_age(year_of_birth)
    149
    150        john = src.person.Person(
    151            # first_name='john',
    152            # last_name='smith',
    153            # year_of_birth=1580,
    154            first_name=first_name,
    155            last_name=last_name,
    156            year_of_birth=year_of_birth,
    157        )
    158
    159        reality = john.say_hello()
    160        my_expectation = (
    161            # 'Hi, my name is jane doe and I am'
    162            # f' {calculate_age(1991)}'
    163            # 'Hi, my name is john smith and I am'
    164            # f' {calculate_age(1580)}'
    165            f'Hi, my name is {first_name} {last_name}'
    166            f' and I am {age}'
    167        )
    168        self.assertEqual(reality, my_expectation)
    

    the test is still green.

  • I do the same thing with a_person

    159        reality = john.say_hello()
    160        my_expectation = (
    161            # 'Hi, my name is jane doe and I am'
    162            # f' {calculate_age(1991)}'
    163            # 'Hi, my name is john smith and I am'
    164            # f' {calculate_age(1580)}'
    165            f'Hi, my name is {first_name} {last_name}'
    166            f' and I am {age}'
    167        )
    168        self.assertEqual(reality, my_expectation)
    169
    170        first_name = 'person'
    171        last_name = 'public'
    172
    173        year_of_birth = 2000
    174        age = calculate_age(year_of_birth)
    175
    176        a_person = src.person.Person(
    177            # first_name='person',
    178            # last_name='public',
    179            # year_of_birth=2000,
    180            first_name=first_name,
    181            last_name=last_name,
    182            year_of_birth=year_of_birth,
    183            sex='F',
    184        )
    185
    186        reality = a_person.say_hello()
    187        my_expectation = (
    188            # 'Hi, my name is john smith and I am'
    189            # f' {calculate_age(1580)}'
    190            # 'Hi, my name is person public and I am'
    191            # f' {calculate_age(2000)}'
    192            f'Hi, my name is {first_name} {last_name}'
    193            f' and I am {age}'
    194        )
    195        self.assertEqual(reality, my_expectation)
    196
    197
    198# Exceptions seen
    

    the test is still green

  • I can add a random person with random values for the first_name, last_name and age variables that are sent in the call to src.person.Person to replace joe, jane, john and a_person since they are all made the same way

     91    def test_classy_person_says_hello(self):
     92        first_name = get_random_name()
     93        last_name = get_random_name()
     94
     95        year_of_birth = get_random_year_of_birth()
     96        age = calculate_age(year_of_birth)
     97
     98        a_random_person = src.person.Person(
     99            first_name=first_name,
    100            last_name=last_name,
    101            year_of_birth=year_of_birth,
    102        )
    103
    104        reality = a_random_person.say_hello()
    105        my_expectation = ''
    106        self.assertEqual(reality, my_expectation)
    107
    108        first_name = 'joe'
    109        last_name = 'blow'
    

    the terminal is my friend and shows AssertionError

    AssertionError: 'Hi, my name is Z Y and I am X' != ''
    
  • I change my_expectation to match reality for a_random_person

    104        reality = a_random_person.say_hello()
    105        # my_expectation = ''
    106        my_expectation = (
    107            f'Hi, my name is {first_name} {last_name}'
    108            f' and I am {age}'
    109        )
    110        self.assertEqual(reality, my_expectation)
    

    the test passes.

  • I remove the commented lines and the other people from test_classy_person_says_hello because a_random_person covers their cases

     91    def test_classy_person_says_hello(self):
     92        first_name = get_random_name()
     93        last_name = get_random_name()
     94
     95        year_of_birth = get_random_year_of_birth()
     96        age = calculate_age(year_of_birth)
     97
     98        a_random_person = src.person.Person(
     99            first_name=first_name,
    100            last_name=last_name,
    101            year_of_birth=year_of_birth,
    102        )
    103
    104        reality = a_random_person.say_hello()
    105        my_expectation = (
    106            f'Hi, my name is {first_name} {last_name}'
    107            f' and I am {age}'
    108        )
    109        self.assertEqual(reality, my_expectation)
    110
    111
    112# Exceptions seen
    113# AssertionError
    114# NameError
    115# AttributeError
    116# TypeError
    117# SyntaxError
    
  • I add a git commit message in the other terminal

    git commit -am \
    'test_classy_person_says_hello with random values'
    

extract random_first_name class attribute

I make the values for first_name in the tests the same way each time, since TestPerson is a class, I can use a class attribute to remove repetition of how I make it, then have all the methods reference it

  • I go back to the terminal that is running the tests

  • I add a class attribute called random_first_name to the TestPerson object

    32class TestPerson(unittest.TestCase):
    33
    34    random_first_name = get_random_name()
    35
    36    def test_factory_w_keyword_arguments(self):
    
  • I use the new class attribute in test_factory_w_keyword_arguments

    36    def test_factory_w_keyword_arguments(self):
    37        year_of_birth = get_random_year_of_birth()
    38
    39        a_person = dict(
    40            # first_name=get_random_name(),
    41            first_name=self.random_first_name,
    42            last_name=get_random_name(),
    43            sex=pick_one('F', 'M'),
    44        )
    

    the test is still green.

  • I use the new class attribute in test_factory_w_optional_arguments

    56    def test_factory_w_optional_arguments(self):
    57        # first_name = get_random_name()
    58        first_name = self.random_first_name
    59        year_of_birth = get_random_year_of_birth()
    60
    61        reality = src.person.factory(
    62            # first_name=first_name,
    63            first_name=self.random_first_name,
    64            year_of_birth=year_of_birth,
    65        )
    66        my_expectation = dict(
    67            # first_name=first_name,
    68            first_name=self.random_first_name,
    69            last_name='doe',
    70            sex='M',
    71            age=calculate_age(year_of_birth),
    72        )
    73        self.assertEqual(reality, my_expectation)
    74
    75    def test_factory_person_says_hello(self):
    

    the test is still green.

  • I use the new class attribute in test_factory_person_says_hello

     75    def test_factory_person_says_hello(self):
     76        # first_name = get_random_name()
     77        first_name = self.random_first_name
     78        last_name = get_random_name()
     79        sex = pick_one('F', 'M')
     80
     81        year_of_birth = get_random_year_of_birth()
     82        age = calculate_age(year_of_birth)
     83
     84        a_random_person = src.person.factory(
     85            # first_name=first_name,
     86            first_name=self.random_first_name,
     87            last_name=last_name,
     88            sex=sex,
     89            year_of_birth=year_of_birth,
     90        )
     91
     92        reality = src.person.say_hello(a_random_person)
     93        my_expectation = (
     94            # f'Hi, my name is {first_name} {last_name}'
     95            f'Hi, my name is {self.random_first_name}'
     96            f' {last_name}'
     97            f' and I am {age}'
     98        )
     99        self.assertEqual(reality, my_expectation)
    100
    101    def test_classy_person_says_hello(self):
    

    the test is still green.

  • I use the new class attribute in test_classy_person_says_hello

    101    def test_classy_person_says_hello(self):
    102        # first_name = get_random_name()
    103        first_name = self.random_first_name
    104        last_name = get_random_name()
    105
    106        year_of_birth = get_random_year_of_birth()
    107        age = calculate_age(year_of_birth)
    108
    109        a_random_person = src.person.Person(
    110            # first_name=first_name,
    111            first_name=self.random_first_name,
    112            last_name=last_name,
    113            year_of_birth=year_of_birth,
    114        )
    115
    116        reality = a_random_person.say_hello()
    117        my_expectation = (
    118            # f'Hi, my name is {first_name} {last_name}'
    119            f'Hi, my name is {self.random_first_name}'
    120            f' {last_name}'
    121            f' and I am {age}'
    122        )
    123        self.assertEqual(reality, my_expectation)
    124
    125
    126# Exceptions seen
    

    the test is still green.

  • I add a git commit message in the other terminal

    git commit -am \
    'extract random_first_name class attribute'
    

extract random_year_of_birth class attribute

I call the get_random_year_of_birth function for year_of_birth in each test, since TestPerson is a class, I can use a class attribute to remove repetition of those calls, then have all the methods reference the value it returns

  • I go back to the terminal that is running the tests

  • I add a class attribute called random_year_of_birth to the TestPerson object

    32class TestPerson(unittest.TestCase):
    33
    34    random_first_name = get_random_name()
    35    random_year_of_birth = get_random_year_of_birth()
    36
    37    def test_factory_w_keyword_arguments(self):
    
  • I use self.random_year_of_birth in test_factory_w_keyword_arguments

    37    def test_factory_w_keyword_arguments(self):
    38        # year_of_birth = get_random_year_of_birth()
    39        year_of_birth = self.random_year_of_birth
    40
    41        a_person = dict(
    42            # first_name=get_random_name(),
    43            first_name=self.random_first_name,
    44            last_name=get_random_name(),
    45            sex=pick_one('F', 'M'),
    46        )
    47
    48        reality = src.person.factory(
    49            **a_person,
    50            # year_of_birth=year_of_birth,
    51            year_of_birth=self.random_year_of_birth,
    52        )
    53        my_expectation = dict(
    54            **a_person,
    55            # age=calculate_age(year_of_birth),
    56            age=calculate_age(
    57                self.random_year_of_birth
    58            ),
    59        )
    60        self.assertEqual(reality, my_expectation)
    61
    62    def test_factory_w_optional_arguments(self):
    

    still green.

  • I use self.random_year_of_birth in test_factory_w_optional_arguments

    62    def test_factory_w_optional_arguments(self):
    63        # first_name = get_random_name()
    64        first_name = self.random_first_name
    65        # year_of_birth = get_random_year_of_birth()
    66        year_of_birth = self.random_year_of_birth
    67
    68        reality = src.person.factory(
    69            # first_name=first_name,
    70            first_name=self.random_first_name,
    71            # year_of_birth=year_of_birth,
    72            year_of_birth=self.random_year_of_birth,
    73        )
    74        my_expectation = dict(
    75            # first_name=first_name,
    76            first_name=self.random_first_name,
    77            last_name='doe',
    78            sex='M',
    79            # age=calculate_age(year_of_birth),
    80            age=calculate_age(
    81                self.random_year_of_birth
    82            ),
    83        )
    84        self.assertEqual(reality, my_expectation)
    85
    86    def test_factory_person_says_hello(self):
    

    still green.

  • I use self.random_year_of_birth in test_factory_person_says_hello

     86    def test_factory_person_says_hello(self):
     87        # first_name = get_random_name()
     88        first_name = self.random_first_name
     89        last_name = get_random_name()
     90        sex = pick_one('F', 'M')
     91
     92        # year_of_birth = get_random_year_of_birth()
     93        # age = calculate_age(year_of_birth)
     94        year_of_birth = self.random_year_of_birth
     95        age = calculate_age(self.random_year_of_birth)
     96
     97        a_random_person = src.person.factory(
     98            # first_name=first_name,
     99            first_name=self.random_first_name,
    100            last_name=last_name,
    101            sex=sex,
    102            # year_of_birth=year_of_birth,
    103            year_of_birth=self.random_year_of_birth,
    104        )
    105
    106        reality = src.person.say_hello(a_random_person)
    107        my_expectation = (
    108            # f'Hi, my name is {first_name} {last_name}'
    109            f'Hi, my name is {self.random_first_name}'
    110            f' {last_name}'
    111            f' and I am {age}'
    112        )
    113        self.assertEqual(reality, my_expectation)
    114
    115    def test_classy_person_says_hello(self):
    

    still green.

  • I use self.random_year_of_birth in test_classy_person_says_hello

    115    def test_classy_person_says_hello(self):
    116        # first_name = get_random_name()
    117        first_name = self.random_first_name
    118        last_name = get_random_name()
    119
    120        # year_of_birth = get_random_year_of_birth()
    121        # age = calculate_age(year_of_birth)
    122        year_of_birth = self.random_year_of_birth
    123        age = calculate_age(self.random_year_of_birth)
    124
    125        a_random_person = src.person.Person(
    126            # first_name=first_name,
    127            first_name=self.random_first_name,
    128            last_name=last_name,
    129            # year_of_birth=year_of_birth,
    130            year_of_birth=self.random_year_of_birth,
    131        )
    

    still green.

  • I add a git commit message in the other terminal

    git commit -am \
    'extract random_year_of_birth class attribute'
    

extract random_last_name class attribute

The last_name variable is made the same way in three of the four tests, I can use a class attribute to remove its repetition then have all the methods reference the value

  • I go back to the terminal that is running the tests

  • I add a class attribute called random_last_name to the TestPerson object

    32class TestPerson(unittest.TestCase):
    33
    34    random_first_name = get_random_name()
    35    random_last_name = get_random_name()
    36    random_year_of_birth = get_random_year_of_birth()
    37
    38    def test_factory_w_keyword_arguments(self):
    
  • I use self.random_last_name in test_factory_w_keyword_arguments

    38    def test_factory_w_keyword_arguments(self):
    39        # year_of_birth = get_random_year_of_birth()
    40        year_of_birth = self.random_year_of_birth
    41
    42        a_person = dict(
    43            # first_name=get_random_name(),
    44            first_name=self.random_first_name,
    45            # last_name=get_random_name(),
    46            last_name=self.random_last_name,
    47            sex=pick_one('F', 'M'),
    48        )
    

    green.

  • I use self.random_last_name in test_factory_person_says_hello

     88    def test_factory_person_says_hello(self):
     89        # first_name = get_random_name()
     90        first_name = self.random_first_name
     91        # last_name = get_random_name()
     92        last_name = self.random_last_name
     93        sex = pick_one('F', 'M')
     94
     95        # year_of_birth = get_random_year_of_birth()
     96        year_of_birth = self.random_year_of_birth
     97        # age = calculate_age(year_of_birth)
     98        age = calculate_age(self.random_year_of_birth)
     99
    100        a_random_person = src.person.factory(
    101            # first_name=first_name,
    102            first_name=self.random_first_name,
    103            # last_name=last_name,
    104            last_name=self.random_last_name,
    105            sex=sex,
    106            # year_of_birth=year_of_birth,
    107            year_of_birth=self.random_year_of_birth,
    108        )
    109
    110        reality = src.person.say_hello(a_random_person)
    111        my_expectation = (
    112            # f'Hi, my name is {first_name} {last_name}'
    113            f'Hi, my name is {self.random_first_name}'
    114            # f' {last_name}'
    115            f' {self.random_last_name}'
    116            f' and I am {age}'
    117        )
    118        self.assertEqual(reality, my_expectation)
    119
    120    def test_classy_person_says_hello(self):
    

    green.

  • I use self.random_last_name in test_classy_person_says_hello

    120    def test_classy_person_says_hello(self):
    121        # first_name = get_random_name()
    122        first_name = self.random_first_name
    123        # last_name = get_random_name()
    124        last_name = self.random_last_name
    125
    126        # year_of_birth = get_random_year_of_birth()
    127        # age = calculate_age(year_of_birth)
    128        year_of_birth = self.random_year_of_birth
    129        age = calculate_age(self.random_year_of_birth)
    130
    131        a_random_person = src.person.Person(
    132            # first_name=first_name,
    133            first_name=self.random_first_name,
    134            # last_name=last_name,
    135            last_name=self.random_last_name,
    136            # year_of_birth=year_of_birth,
    137            year_of_birth=self.random_year_of_birth,
    138        )
    139
    140        reality = a_random_person.say_hello()
    141        my_expectation = (
    142            # f'Hi, my name is {first_name} {last_name}'
    143            f'Hi, my name is {self.random_first_name}'
    144            # f' {last_name}'
    145            f' {self.random_last_name}'
    146            f' and I am {age}'
    147        )
    148        self.assertEqual(reality, my_expectation)
    149
    150
    151# Exceptions seen
    

    green.

  • I add a git commit message in the other terminal

    git commit -am \
    'extract random_last_name class attribute'
    

extract age class attribute

I call the calculate_age function with the self.random_year_of_birth attribute in each test, since TestPerson is a class, I can use a class attribute to remove repetition of those calls, then have all the methods reference the value it returns

  • I go back to the terminal that is running the tests

  • I add a class attribute called age to the TestPerson object

    32class TestPerson(unittest.TestCase):
    33
    34    random_first_name = get_random_name()
    35    random_last_name = get_random_name()
    36    random_year_of_birth = get_random_year_of_birth()
    37    age = calculate_age(random_year_of_birth)
    38
    39    def test_factory_w_keyword_arguments(self):
    
  • I use self.age in test_factory_w_keyword_arguments

    51        reality = src.person.factory(
    52            **a_person,
    53            # year_of_birth=year_of_birth,
    54            year_of_birth=self.random_year_of_birth,
    55        )
    56        my_expectation = dict(
    57            **a_person,
    58            # age=calculate_age(year_of_birth),
    59            # age=calculate_age(
    60            #     self.random_year_of_birth
    61            # ),
    62            age=self.age,
    63        )
    64        self.assertEqual(reality, my_expectation)
    65
    66    def test_factory_w_optional_arguments(self):
    

    still green.

  • I remove the commented lines and unused variables from test_factory_w_keyword_arguments

    39    def test_factory_w_keyword_arguments(self):
    40        a_person = dict(
    41            first_name=self.random_first_name,
    42            last_name=self.random_last_name,
    43            sex=pick_one('F', 'M'),
    44        )
    45
    46        reality = src.person.factory(
    47            **a_person,
    48            year_of_birth=self.random_year_of_birth,
    49        )
    50        my_expectation = dict(
    51            **a_person,
    52            age=self.age,
    53        )
    54        self.assertEqual(reality, my_expectation)
    55
    56    def test_factory_w_optional_arguments(self):
    
  • I use self.age in test_factory_w_optional_arguments

    62        reality = src.person.factory(
    63            # first_name=first_name,
    64            first_name=self.random_first_name,
    65            # year_of_birth=year_of_birth,
    66            year_of_birth=self.random_year_of_birth,
    67        )
    68        my_expectation = dict(
    69            # first_name=first_name,
    70            first_name=self.random_first_name,
    71            last_name='doe',
    72            sex='M',
    73            # age=calculate_age(year_of_birth),
    74            # age=calculate_age(
    75            #     self.random_year_of_birth
    76            # ),
    77            age=self.age,
    78        )
    79        self.assertEqual(reality, my_expectation)
    80
    81    def test_factory_person_says_hello(self):
    

    still green.

  • I remove the commented lines and unused variables from test_factory_w_optional_arguments

    56    def test_factory_w_optional_arguments(self):
    57        reality = src.person.factory(
    58            first_name=self.random_first_name,
    59            year_of_birth=self.random_year_of_birth,
    60        )
    61        my_expectation = dict(
    62            first_name=self.random_first_name,
    63            last_name='doe',
    64            sex='M',
    65            age=self.age,
    66        )
    67        self.assertEqual(reality, my_expectation)
    68
    69    def test_factory_person_says_hello(self):
    
  • I use self.age in test_factory_person_says_hello

     69    def test_factory_person_says_hello(self):
     70        # first_name = get_random_name()
     71        first_name = self.random_first_name
     72        # last_name = get_random_name()
     73        last_name = self.random_last_name
     74        sex = pick_one('F', 'M')
     75
     76        # year_of_birth = get_random_year_of_birth()
     77        year_of_birth = self.random_year_of_birth
     78        # age = calculate_age(year_of_birth)
     79        # age = calculate_age(self.random_year_of_birth)
     80        age = self.age
     81
     82        a_random_person = src.person.factory(
     83            # first_name=first_name,
     84            first_name=self.random_first_name,
     85            # last_name=last_name,
     86            last_name=self.random_last_name,
     87            sex=sex,
     88            # year_of_birth=year_of_birth,
     89            year_of_birth=self.random_year_of_birth,
     90        )
     91
     92        reality = src.person.say_hello(a_random_person)
     93        my_expectation = (
     94            # f'Hi, my name is {first_name} {last_name}'
     95            f'Hi, my name is {self.random_first_name}'
     96            # f' {last_name}'
     97            f' {self.random_last_name}'
     98            # f' and I am {age}'
     99            f' and I am {self.age}'
    100        )
    101        self.assertEqual(reality, my_expectation)
    102
    103    def test_classy_person_says_hello(self):
    

    still green.

  • I remove the commented lines and unused variables from test_factory_person_says_hello

    69    def test_factory_person_says_hello(self):
    70        a_random_person = src.person.factory(
    71            first_name=self.random_first_name,
    72            last_name=self.random_last_name,
    73            year_of_birth=self.random_year_of_birth,
    74        )
    75
    76        reality = src.person.say_hello(a_random_person)
    77        my_expectation = (
    78            f'Hi, my name is {self.random_first_name}'
    79            f' {self.random_last_name}'
    80            f' and I am {self.age}'
    81        )
    82        self.assertEqual(reality, my_expectation)
    83
    84    def test_classy_person_says_hello(self):
    

    I remove the sex variable and parameter because it is not used in this test. I guess a_random_person is not all that random since it will always have 'M' as sex in this test.

  • I use self.age in test_classy_person_says_hello

     90        # year_of_birth = get_random_year_of_birth()
     91        # age = calculate_age(year_of_birth)
     92        year_of_birth = self.random_year_of_birth
     93        # age = calculate_age(self.random_year_of_birth)
     94        age = self.age
     95
     96        a_random_person = src.person.Person(
     97            # first_name=first_name,
     98            first_name=self.random_first_name,
     99            # last_name=last_name,
    100            last_name=self.random_last_name,
    101            # year_of_birth=year_of_birth,
    102            year_of_birth=self.random_year_of_birth,
    103        )
    104
    105        reality = a_random_person.say_hello()
    106        my_expectation = (
    107            # f'Hi, my name is {first_name} {last_name}'
    108            f'Hi, my name is {self.random_first_name}'
    109            # f' {last_name}'
    110            f' {self.random_last_name}'
    111            # f' and I am {age}'
    112            f' and I am {self.age}'
    113        )
    114        self.assertEqual(reality, my_expectation)
    115
    116
    117# Exceptions seen
    

    still green.

  • I remove the commented lines and unused variables from test_classy_person_says_hello

     84    def test_classy_person_says_hello(self):
     85        a_random_person = src.person.Person(
     86            first_name=self.random_first_name,
     87            last_name=self.random_last_name,
     88            year_of_birth=self.random_year_of_birth,
     89        )
     90
     91        reality = a_random_person.say_hello()
     92        my_expectation = (
     93            f'Hi, my name is {self.random_first_name}'
     94            f' {self.random_last_name}'
     95            f' and I am {self.age}'
     96        )
     97        self.assertEqual(reality, my_expectation)
     98
     99
    100# Exceptions seen
    

    I also do not need the sex variable and parameter in this test.

  • I add a git commit message in the other terminal

    git commit -am \
    'extract age class attribute'
    

I can remove repetition with class attributes


how to use the setUp method to reset class attributes for every test

A problem with the current setup with the class attributes is that they are made once when the class is initialized. This means that even though they all use random values, those values are created once and every test that references the values after that is using the exact same values for each test.

I want each test to get new random values every time they run and the unittest.TestCase class has a way to do that - the setUp method, it runs before every test is run.


RED: make it fail


  • I go back to the terminal that is running the tests

  • I add the unittest.TestCase.setUp method to TestPerson then move the class attributes into it

    32class TestPerson(unittest.TestCase):
    33
    34    # random_first_name = get_random_name()
    35    # random_last_name = get_random_name()
    36    # random_year_of_birth = get_random_year_of_birth()
    37    # age = calculate_age(random_year_of_birth)
    38
    39    def setUp(self):
    40        random_first_name = get_random_name()
    41        random_last_name = get_random_name()
    42        random_year_of_birth = get_random_year_of_birth()
    43        age = calculate_age(random_year_of_birth)
    44
    45    def test_factory_w_keyword_arguments(self):
    

    the terminal is my friend, and shows AttributeError

    FAILED ...test_classy_person_says_hello -
        AttributeError: 'TestPerson' object has
        no attribute 'random_first_name'
    FAILED ...test_factory_person_says_hello -
        AttributeError: 'TestPerson' object has
        no attribute 'random_first_name'
    FAILED ...test_factory_w_keyword_arguments -
        AttributeError: 'TestPerson' object has
        no attribute 'random_first_name'
    FAILED ...test_factory_w_optional_arguments -
        AttributeError: 'TestPerson' object has
        no attribute 'random_first_name'
    

    because the first_name variable now belongs to the setUp method, the other methods have no way to reach it. I have to make it a class attribute.


GREEN: make it pass


  • I change the first_name variable to a class attribute in the setUp method for the test methods to be able to use it

    39  def setUp(self):
    40      # random_first_name = get_random_name()
    41      self.random_first_name = get_random_name()
    42      random_last_name = get_random_name()
    43      random_year_of_birth = get_random_year_of_birth()
    44      age = calculate_age(random_year_of_birth)
    45
    46  def test_factory_w_keyword_arguments(self):
    

    the terminal is my friend, and shows AttributeError

    FAILED ...test_classy_person_says_hello - AttributeError:
        'TestPerson' object has no attribute 'random_last_name'.
        Did you mean: 'random_first_name'?
    FAILED ...test_factory_person_says_hello - AttributeError:
        'TestPerson' object has no attribute 'random_last_name'.
        Did you mean: 'random_first_name'?
    FAILED ...test_factory_w_keyword_arguments - AttributeError:
        'TestPerson' object has no attribute 'random_last_name'.
        Did you mean: 'random_first_name'?
    FAILED ...test_factory_w_optional_arguments -
        AttributeError: 'TestPerson' object has
        no attribute 'random_year_of_birth'.
    

    because the year_of_birth and last_name variables now belong to the setUp method, the other methods have no way to reach them. I have to make them class attributes

  • I change the year_of_birth and last_name variables to class attributes in the setUp method for the test methods to be able to use them as well

    39    def setUp(self):
    40        # random_first_name = get_random_name()
    41        self.random_first_name = get_random_name()
    42        # random_last_name = get_random_name()
    43        self.random_last_name = get_random_name()
    44        # random_year_of_birth = get_random_year_of_birth()
    45        # age = calculate_age(random_year_of_birth)
    46        self.random_year_of_birth = (
    47            get_random_year_of_birth()
    48        )
    49        age = calculate_age(
    50            self.random_year_of_birth
    51        )
    52
    53    def test_factory_w_keyword_arguments(self):
    

    the terminal is my friend, and shows AttributeError

    FAILED ...test_classy_person_says_hello -
        AttributeError: 'TestPerson' object has
        no attribute 'age'
    FAILED ...test_factory_person_says_hello -
        AttributeError: 'TestPerson' object has
        no attribute 'age'
    FAILED ...test_factory_w_keyword_arguments -
        AttributeError: 'TestPerson' object has
        no attribute 'age'
    FAILED ...test_factory_w_optional_arguments -
        AttributeError: 'TestPerson' object has
        no attribute 'age'
    

    because the age belongs to the setUp method, and the other methods have no way to reach it. I have to make it a class attribute

  • I change the age variable to class attributes in the setUp method for the other methods to be able to use it

    39    def setUp(self):
    40        # random_first_name = get_random_name()
    41        self.random_first_name = get_random_name()
    42        # random_last_name = get_random_name()
    43        self.random_last_name = get_random_name()
    44        # random_year_of_birth = get_random_year_of_birth()
    45        # age = calculate_age(random_year_of_birth)
    46        self.random_year_of_birth = (
    47            get_random_year_of_birth()
    48        )
    49        # age = calculate_age(
    50        self.age = calculate_age(
    51            self.random_year_of_birth
    52        )
    53
    54    def test_factory_w_keyword_arguments(self):
    

    the test passes.

The unittest.TestCase.setUp method runs before every test, in this case it sets these class attributes (variables) to new values before every test

  • self.random_first_name to the result of calling the get_random_name function, which returns a random name

  • self.random_last_name to the result of calling the get_random_name function, which returns a random name

  • self.random_year_of_birth to the result of calling the get_random_year_of_birth function which returns a random year between 120 years ago and the current year

  • self.age to the result of calling the calculate_age function, which returns the current year minus self.random_year_of_birth


REFACTOR: make it better


  • I no longer need the calculate_age function because it is only called by the setUp method. I use `datetime.datetime.now`_ directly

    39    def setUp(self):
    40        # random_first_name = get_random_name()
    41        self.random_first_name = get_random_name()
    42        # random_last_name = get_random_name()
    43        self.random_last_name = get_random_name()
    44        # random_year_of_birth = get_random_year_of_birth()
    45        # age = calculate_age(random_year_of_birth)
    46        self.random_year_of_birth = (
    47            get_random_year_of_birth()
    48        )
    49        # age = calculate_age(
    50        # self.age = calculate_age(
    51        #     self.random_year_of_birth
    52        # )
    53        self.age = (
    54            datetime.datetime.now().year
    55          - self.random_year_of_birth
    56        )
    57
    58    def test_factory_w_keyword_arguments(self):
    

    the test is still green.

  • I no longer need the get_random_year_of_birth function because it is only called by the setUp method. I use random.randint directly

    39    def setUp(self):
    40        # random_first_name = get_random_name()
    41        self.random_first_name = get_random_name()
    42        # random_last_name = get_random_name()
    43        self.random_last_name = get_random_name()
    44        # random_year_of_birth = get_random_year_of_birth()
    45        # age = calculate_age(random_year_of_birth)
    46        # self.random_year_of_birth = (
    47        #     get_random_year_of_birth()
    48        # )
    49        this_year = datetime.datetime.now().year
    50        self.random_year_of_birth = random.randint(
    51            this_year-120, this_year
    52        )
    53        # age = calculate_age(
    54        # self.age = calculate_age(
    55        #     self.random_year_of_birth
    56        # )
    57        self.age = (
    58            datetime.datetime.now().year
    59          - self.random_year_of_birth
    60        )
    61
    62    def test_factory_w_keyword_arguments(self):
    

    still green.

  • I use the this_year variable in the calculation for self.age

    39    def setUp(self):
    40        # random_first_name = get_random_name()
    41        self.random_first_name = get_random_name()
    42        # random_last_name = get_random_name()
    43        self.random_last_name = get_random_name()
    44        # random_year_of_birth = get_random_year_of_birth()
    45        # age = calculate_age(random_year_of_birth)
    46        # self.random_year_of_birth = (
    47        #     get_random_year_of_birth()
    48        # )
    49        this_year = datetime.datetime.now().year
    50        self.random_year_of_birth = random.randint(
    51            this_year-120, this_year
    52        )
    53        # age = calculate_age(
    54        # self.age = calculate_age(
    55        #     self.random_year_of_birth
    56        # )
    57        # self.age = (
    58        #     datetime.datetime.now().year
    59        #   - self.random_year_of_birth
    60        # )
    61        self.age = this_year - self.random_year_of_birth
    62
    63    def test_factory_w_keyword_arguments(self):
    

    green. datetime.datetime.now().year is now called once each time the setUp method runs

  • I remove the commented lines

    32class TestPerson(unittest.TestCase):
    33
    34    def setUp(self):
    35        self.random_first_name = get_random_name()
    36        self.random_last_name = get_random_name()
    37        this_year = datetime.datetime.now().year
    38        self.random_year_of_birth = random.randint(
    39            this_year-120, this_year
    40        )
    41        self.age = this_year - self.random_year_of_birth
    42
    43    def test_factory_w_keyword_arguments(self):
    
  • I remove the calculate_age and get_random_year_of_birth functions

     1import datetime
     2import random
     3import src.person
     4import unittest
     5
     6
     7def pick_one(*choices):
     8    return random.choice(choices)
     9
    10
    11def get_random_name():
    12    return pick_one(
    13        'jane', 'joe', 'john', 'person',
    14        'doe', 'smith', 'blow', 'public',
    15    )
    16
    17
    18class TestPerson(unittest.TestCase):
    
  • I add a git commit message in the other terminal

    git commit -am 'move class attributes to setUp method'
    

test_attributes_and_methods_of_person_instance

Python has the dir built-in function which shows the attributes and methods of the object it is given in parentheses. It allows me to see what makes up an object without looking at the code or reading the documentation. I can then run tests to see what each thing does.


RED: make it fail


  • I go back to the terminal that is running the tests

  • I add a new test with the dir built-in function in test_person.py

    81        reality = a_random_person.say_hello()
    82        my_expectation = (
    83            f'Hi, my name is {self.random_first_name}'
    84            f' {self.random_last_name}'
    85            f' and I am {self.age}'
    86        )
    87        self.assertEqual(reality, my_expectation)
    88
    89    def test_attributes_and_methods_of_person_class(self):
    90        reality = dir(src.person.Person)
    91        my_expectation = None
    92        self.assertEqual(reality, my_expectation)
    93
    94
    95# Exceptions seen
    

    the terminal is my friend, and shows AssertionError

    AssertionError: Lists differ:
        ['__class__', '__delattr__', '__dict__',
         '[377 chars]llo']
     != None
    

    because dir returned a list (anything in square brackets [ ]) and my_expectation is None


GREEN: make it pass


  • I copy (ctrl/command+c) the values from the terminal and paste (ctrl/command+v) them as my_expectation

    89    def test_attributes_and_methods_of_person_class(self):
    90        reality = dir(src.person.Person)
    91        # my_expectation = None
    92        my_expectation = [
    93            '__class__', '__delattr__', '__dict__',
    94            [371 chars]llo'
    95        ]
    96        self.assertEqual(reality, my_expectation)
    97
    98
    99# Exceptions seen
    

    the terminal is my friend, and shows SyntaxError

    E       [371 chars]llo'
    E                     ^
    E   SyntaxError: unterminated string literal
                     (detected at line 94)
    

    because I have a closing quote (') without a matching opening one and enclosures must be closed once open

  • I add the opening quote

    89    def test_attributes_and_methods_of_person_class(self):
    90        reality = dir(src.person.Person)
    91        # my_expectation = None
    92        my_expectation = [
    93            '__class__', '__delattr__', '__dict__',
    94            '[371 chars]llo'
    95        ]
    96        self.assertEqual(reality, my_expectation)
    97
    98
    99# Exceptions seen
    

    the terminal is my friend and shows AssertionError

    AssertionError: Lists differ:
        ['__c[32 chars]_', '__dir__', '__doc__', '__eq__',
         '__firstli[329 chars]llo']
     != ['__c[32 chars]_', '[371 chars]llo']
    

    it shows me the entire list below the message

  • I copy (ctrl/command+c) the values from the terminal and paste (ctrl/command+v) them as my_expectation

     89    def test_attributes_and_methods_of_person_class(self):
     90        reality = dir(src.person.Person)
     91        # my_expectation = None
     92        # my_expectation = [
     93        #     '__class__', '__delattr__', '__dict__',
     94        #     '[371 chars]llo'
     95        # ]
     96        my_expectation = E       - ['__class__',
     97E       -  '__delattr__',
     98E       -  '__dict__',
     99E       -  '__dir__',
    100E       -  '__doc__',
    101E       -  '__eq__',
    102E       -  '__firstlineno__',
    103E       -  '__format__',
    104E       -  '__ge__',
    105E       -  '__getattribute__',
    106E       -  '__getstate__',
    107E       -  '__gt__',
    108E       -  '__hash__',
    109E       -  '__init__',
    110E       -  '__init_subclass__',
    111E       -  '__le__',
    112E       -  '__lt__',
    113E       -  '__module__',
    114E       -  '__ne__',
    115E       -  '__new__',
    116E       -  '__reduce__',
    117E       -  '__reduce_ex__',
    118E       -  '__repr__',
    119E       -  '__setattr__',
    120E       -  '__sizeof__',
    121E       -  '__static_attributes__',
    122E       -  '__str__',
    123E       -  '__subclasshook__',
    124E       -  '__weakref__',
    125E       -  'say_hello']
    126        self.assertEqual(reality, my_expectation)
    127
    128
    129# Exceptions seen
    

    the terminal shows NameError

    NameError: name 'E' is not defined
    
  • I use the find and replace feature of the Integrated Development Environment (IDE) to remove the extra characters, then remove the commented lines

     89    def test_attributes_and_methods_of_person_class(self):
     90        reality = dir(src.person.Person)
     91        my_expectation = [
     92            '__class__',
     93            '__delattr__',
     94            '__dict__',
     95            '__dir__',
     96            '__doc__',
     97            '__eq__',
     98            '__firstlineno__',
     99            '__format__',
    100            '__ge__',
    101            '__getattribute__',
    102            '__getstate__',
    103            '__gt__',
    104            '__hash__',
    105            '__init__',
    106            '__init_subclass__',
    107            '__le__',
    108            '__lt__',
    109            '__module__',
    110            '__ne__',
    111            '__new__',
    112            '__reduce__',
    113            '__reduce_ex__',
    114            '__repr__',
    115            '__setattr__',
    116            '__sizeof__',
    117            '__static_attributes__',
    118            '__str__',
    119            '__subclasshook__',
    120            '__weakref__',
    121            'say_hello',
    122        ]
    123        self.assertEqual(reality, my_expectation)
    124
    125
    126# Exceptions seen
    
    • the test passes.

    • the __init__ and say_hello methods I defined are in the list

    • there are names in the list that I did not define, which leads to the question of where did they come from?

    • The attributes I defined in the __init__ method are not in the list, because the test called dir on src.person.Person which is the class, not an instance (copy) of the class

  • I add a git commit message in the other terminal

    git commit -am \
    'add test_attributes_and_methods_of_person_class'
    

test_attributes_and_methods_of_person_class


RED: make it fail


I add a test for the attributes and methods of an instance/copy of the Person class to see the difference between it and the original

118            '__str__',
119            '__subclasshook__',
120            '__weakref__',
121            'say_hello',
122        ]
123        self.assertEqual(reality, my_expectation)
124
125    def test_attributes_and_methods_of_person_instance(self):
126        an_instance_of_person = src.person.Person(
127            first_name=self.random_first_name,
128            last_name=self.random_last_name,
129            year_of_birth=self.random_year_of_birth,
130            sex=pick_one('F', 'M')
131        )
132
133        reality = dir(an_instance_of_person)
134        my_expectation = [
135            '__class__',
136            '__delattr__',
137            '__dict__',
138            '__dir__',
139            '__doc__',
140            '__eq__',
141            '__firstlineno__',
142            '__format__',
143            '__ge__',
144            '__getattribute__',
145            '__getstate__',
146            '__gt__',
147            '__hash__',
148            '__init__',
149            '__init_subclass__',
150            '__le__',
151            '__lt__',
152            '__module__',
153            '__ne__',
154            '__new__',
155            '__reduce__',
156            '__reduce_ex__',
157            '__repr__',
158            '__setattr__',
159            '__sizeof__',
160            '__static_attributes__',
161            '__str__',
162            '__subclasshook__',
163            '__weakref__',
164            'say_hello',
165        ]
166        self.assertEqual(reality, my_expectation)
167
168
169# Exceptions

the terminal is my friend, and shows AssertionError

AssertionError: Lists differ:
    ['__c[393 chars]ef__', 'first_name', 'last_name',
      'say_hello', 'year_of_birth']
 != ['__c[393 chars]ef__', 'say_hello']

because first_name, last_name and year_of_birth are missing. Why is there no sex?


GREEN: make it pass


I add the missing attributes to my_expectation

133        reality = dir(an_instance_of_person)
134        my_expectation = [
135            '__class__',
136            '__delattr__',
137            '__dict__',
138            '__dir__',
139            '__doc__',
140            '__eq__',
141            '__firstlineno__',
142            '__format__',
143            '__ge__',
144            '__getattribute__',
145            '__getstate__',
146            '__gt__',
147            '__hash__',
148            '__init__',
149            '__init_subclass__',
150            '__le__',
151            '__lt__',
152            '__module__',
153            '__ne__',
154            '__new__',
155            '__reduce__',
156            '__reduce_ex__',
157            '__repr__',
158            '__setattr__',
159            '__sizeof__',
160            '__static_attributes__',
161            '__str__',
162            '__subclasshook__',
163            '__weakref__',
164            'first_name',
165            'last_name',
166            'say_hello',
167            'year_of_birth',
168        ]
169        self.assertEqual(reality, my_expectation)
170
171
172# Exceptions seen

the test passes.


REFACTOR: make it better


  • I add sex to the list

    133        reality = dir(an_instance_of_person)
    134        my_expectation = [
    135            '__class__',
    136            '__delattr__',
    137            '__dict__',
    138            '__dir__',
    139            '__doc__',
    140            '__eq__',
    141            '__firstlineno__',
    142            '__format__',
    143            '__ge__',
    144            '__getattribute__',
    145            '__getstate__',
    146            '__gt__',
    147            '__hash__',
    148            '__init__',
    149            '__init_subclass__',
    150            '__le__',
    151            '__lt__',
    152            '__module__',
    153            '__ne__',
    154            '__new__',
    155            '__reduce__',
    156            '__reduce_ex__',
    157            '__repr__',
    158            '__setattr__',
    159            '__sizeof__',
    160            '__static_attributes__',
    161            '__str__',
    162            '__subclasshook__',
    163            '__weakref__',
    164            'first_name',
    165            'last_name',
    166            'say_hello',
    167            'sex',
    168            'year_of_birth',
    169        ]
    170        self.assertEqual(reality, my_expectation)
    

    the terminal is my friend, and shows AssertionError

    AssertionError: Lists differ:
        ['__c[400 chars]'first_name', 'last_name',
         'say_hello', 'year_of_birth']
     != ['__c[400 chars]'first_name', 'last_name',
         'say_hello', 'sex', 'year_of_birth']
    

    the sex attribute is not defined anywhere in the Person class

  • I add self.sex to the __init__ method of the Person class in person.py

    41    # pass
    42    # def __init__():
    43    # def __init__(first_name):
    44    # def __init__(self, first_name):
    45    # def __init__(self, first_name, last_name):
    46    def __init__(
    47        # self, first_name, last_name,
    48        # self, first_name, last_name=None,
    49        self, first_name, last_name='doe',
    50        # year_of_birth,
    51        # year_of_birth, sex,
    52        # year_of_birth, sex=None,
    53        year_of_birth=None, sex=None,
    54    ):
    55        # first_name = first_name
    56        self.first_name = first_name
    57        # last_name = last_name
    58        self.last_name = last_name
    59        # year_of_birth = year_of_birth
    60        self.year_of_birth = year_of_birth
    61        self.sex = sex
    62        return None
    

    the test passes

  • I remove the commented lines

     1import datetime
     2
     3
     4def calculate_age(year_of_birth):
     5    return (
     6        datetime.datetime.today().year
     7      - year_of_birth
     8    )
     9
    10
    11def say_hello(a_dictionary):
    12    return (
    13        f'Hi, my name is {a_dictionary.get("first_name")}'
    14        f' {a_dictionary.get("last_name")}'
    15        f' and I am {a_dictionary.get("age")}'
    16    )
    17
    18
    19def factory(
    20        first_name, year_of_birth,
    21        last_name='doe', sex='M',
    22    ):
    23    return {
    24        'first_name': first_name,
    25        'last_name': last_name,
    26        'sex': sex,
    27        'age': calculate_age(year_of_birth),
    28    }
    29
    30
    31class Person:
    32
    33    def __init__(
    34        self, first_name, last_name='doe',
    35        year_of_birth=None, sex=None,
    36    ):
    37        self.first_name = first_name
    38        self.last_name = last_name
    39        self.year_of_birth = year_of_birth
    40        self.sex = sex
    41        return None
    42
    43    def say_hello(self):
    44        age = calculate_age(self.year_of_birth)
    45        return (
    46            f'Hi, my name is {self.first_name}'
    47            f' {self.last_name}'
    48            f' and I am {age}'
    49        )
    
  • I add a git commit message in the other terminal

    git commit -am \
    'add test_attributes_and_methods_of_person_instance'
    

close the project

  • I close test_person.py and person.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


code from the chapter

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


review

There are few problems with what I have now

  • anyone seeing the tests for the first time has to read the class attributes

    class TestPerson(unittest.TestCase):
    
        def setUp(self):
            self.random_first_name = get_random_name()
            self.random_last_name = get_random_name()
            this_year = datetime.datetime.now().year
            self.random_year_of_birth = random.randint(
                this_year-120, this_year
            )
            self.age = this_year - self.random_year_of_birth
    
        def test_factory_w_keyword_arguments(self):
    

    to know how they are created then referenced in each test

  • test_factory_w_keyword_arguments needs the person reading the test to know about double starred expressions

    src.person.factory(
        **a_person,
        year_of_birth=self.random_year_of_birth,
    )
    
    dict(
        **a_person,
        age=self.age,
    )
    

    to know why using the dictionary works in the call to src.person.factory and inside another dictionary

  • test_factory_person_says_hello and test_classy_person_says_hello need the person reading the test to know about f-strings

    f'Hi, my name is {self.random_first_name}'
    f' {self.random_last_name} '
    f'and I am {self.age}'
    

To review

Tip

  • when I find myself writing or doing the same thing two times, I write a function

  • when I find I have two functions that use the same information, I write a class

How many questions can you answer about classes?


what is next?

You have gone through a lot of things and know:

Would you like to test what causes AttributeError or Would you like to know where the extra attributes and methods of the Person class came from?

You know enough to go into the world and use Python. If you stopped going through the book at this point, you would be fine because you know how to make classes, functions and can make dictionaries which is what is behind a lot of the things you will encounter.


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.