how to make a person


This is an exercise in making dictionaries with functions. I think these are the 2 most important concepts in Python

requirements

  • I open a terminal to run makePythonTdd.sh with person as the name of the project

    ./makePythonTdd.sh person
    

    on Windows without Windows Subsystem Linux use makePythonTdd.ps1

    ./makePythonTdd.ps1 person
    

    it makes the folders and files that are needed, installs packages, runs the first test, and the terminal shows AssertionError

    E       AssertionError: True is not false
    
    tests/test_person.py:7: AssertionError
    
  • I hold ctrl (windows/linux) or option (mac) on the keyboard and use the mouse to click on tests/test_person.py:7 to open it in the editor

  • then I change True to False to make the test pass

    7self.assertFalse(False)
    
  • I change the name of the class to match the CapWords format

    4class TestPerson(unittest.TestCase):
    

test_takes_keyword_arguments

red: make it fail

  • I change test_failure to test_takes_keyword_arguments

     1import unittest
     2
     3
     4class TestPerson(unittest.TestCase):
     5
     6    def test_takes_keyword_arguments(self):
     7        self.assertEqual(
     8            src.person.factory(),
     9            None
    10        )
    

    the terminal shows NameError

    NameError: name 'src' is not defined
    

green: make it pass

  • I add it to the list of Exceptions encountered in test_person.py

    13# Exceptions Encountered
    14# AssertionError
    15# NameError
    
  • then I add an import statement for the person module at the top of the file

    1import src.person
    2import unittest
    

    the terminal shows AttributeError

    AttributeError: module 'src.person' has no attribute 'factory'
    
  • I add it to the list of Exceptions encountered

    14# Exceptions Encountered
    15# AssertionError
    16# NameError
    17# AttributeError
    
  • I click on person.py in the src folder to open it in the editor then I add a function

    1def factory():
    2    return None
    

    the test passes

  • I want the function to take in a keyword argument named first_name. I add it to the test in test_person.py

     7    def test_takes_keyword_arguments(self):
     8        self.assertEqual(
     9            src.person.factory(
    10                first_name='first_name',
    11            ),
    12            None
    13        )
    

    the terminal shows TypeError

    TypeError: factory() got an unexpected keyword argument 'first_name'
    
  • I add the error to the list of Exceptions encountered in test_person.py

    16# Exceptions Encountered
    17# AssertionError
    18# NameError
    19# AttributeError
    20# TypeError
    
  • then I add first_name as an input parameter to the function in person.py

    1def factory(first_name):
    2    return None
    

    the test passes

  • I want the function to take in a keyword argument named last_name. I add it to the test in test_person.py

     7    def test_takes_keyword_arguments(self):
     8        self.assertEqual(
     9            src.person.factory(
    10                first_name='first_name',
    11                last_name='last_name',
    12            ),
    13            None
    14        )
    

    the terminal shows TypeError

    TypeError: factory() got an unexpected keyword argument 'last_name'
    
  • I add last_name to the function definition in person.py

    1def factory(first_name, last_name):
    2    return None
    

    the test passes

  • I want the function to take in a keyword argument named sex. I add it to the test in test_person.py

     7    def test_takes_keyword_arguments(self):
     8        self.assertEqual(
     9            src.person.factory(
    10                first_name='first_name',
    11                last_name='last_name',
    12                sex='M',
    13            ),
    14            None
    15        )
    

    the terminal shows TypeError

    TypeError: factory() got an unexpected keyword argument 'sex'
    
  • I add sex as input to the function in person.py

    1def factory(
    2        first_name, last_name,
    3        sex,
    4    ):
    5    return None
    

    the test passes

  • I want the function to take in a keyword argument for year_of_birth and give it the result of calling another function. I add it to the test in test_person.py

     7    def test_takes_keyword_arguments(self):
     8        self.assertEqual(
     9            src.person.factory(
    10                first_name='first_name',
    11                last_name='last_name',
    12                sex='M',
    13                year_of_birth=this_year(),
    14            ),
    15            None
    16        )
    

    the terminal shows NameError

    NameError: name 'this_year' is not defined
    
  • I add a definition for the this_year function above the class definition in test_person.py

    Note

    the …(ellipsis) represents code that does not need to change in this part

     1import unittest
     2import src.person
     3
     4
     5def this_year():
     6    return None
     7
     8
     9class TestPerson(unittest.TestCase):
    10
    11    def test_takes_keyword_arguments(self):
    12        ...
    

    the terminal shows TypeError

    TypeError: factory() got an unexpected keyword argument 'year_of_birth'
    

    when I add the name to the function definition in person.py

    1def factory(
    2        first_name, last_name,
    3        sex, year_of_birth,
    4    ):
    5    return None
    

    the test passes

  • I want the factory function to return a dictionary as output, I change the expectation in the assertion in test_person.py

    11    def test_takes_keyword_arguments(self):
    12        self.assertEqual(
    13            src.person.factory(
    14                first_name='first_name',
    15                last_name='last_name',
    16                sex='M',
    17                year_of_birth=this_year(),
    18            ),
    19            dict()
    20        )
    

    the terminal shows AssertionError

    AssertionError: None != {}
    

    I change the return statement in person.py

    1def factory(
    2        first_name, last_name,
    3        sex, year_of_birth,
    4    ):
    5    return {}
    

    the test passes because {} and dict() are two ways to write an empty dictionary

  • I want the expected dictionary in the test to have a key named first_name with the same value as what is given when the factory function is called. I change it in test_person.py

    11    def test_takes_keyword_arguments(self):
    12        self.assertEqual(
    13            src.person.factory(
    14                first_name='first_name',
    15                last_name='last_name',
    16                sex='M',
    17                year_of_birth=this_year(),
    18            ),
    19            dict(
    20                first_name='first_name',
    21            )
    22        )
    

    the terminal shows AssertionError

    AssertionError: {} != {'first_name': 'first_name'}
    
  • I change the return statement in person.py

    1def factory(
    2        first_name, last_name,
    3        sex, year_of_birth,
    4    ):
    5    return {'first_name': 'first_name'}
    

    the test passes

  • 'first_name' appears twice in the test, which means I have to make a change in 2 places if I want a different value for it. I add a variable to remove the repetition in test_person.py

    11    def test_takes_keyword_arguments(self):
    12        first_name = 'first_name'
    13
    14        self.assertEqual(
    15            src.person.factory(
    16                first_name=first_name,
    17                last_name='last_name',
    18                sex='M',
    19                year_of_birth=this_year()
    20            ),
    21            dict(
    22                first_name=first_name,
    23            )
    24        )
    

    the test is still green. I now only need to change the value of first_name in one place

    11    def test_takes_keyword_arguments(self):
    12        first_name = 'jane'
    13
    14        self.assertEqual(
    15            src.person.factory(
    16                first_name=first_name,
    17                last_name='last_name',
    18                sex='M',
    19                year_of_birth=this_year()
    20            ),
    21            dict(
    22                first_name=first_name,
    23            )
    24        )
    

    the terminal shows AssertionError

    AssertionError: {'first_name': 'first_name'} != {'first_name': 'jane'}
    
  • I change the return statement in person.py

    1def factory(
    2        first_name, last_name,
    3        sex, year_of_birth,
    4    ):
    5    return {'first_name': 'jane'}
    

    and the test is green again

  • I want the dictionary to have a key named last_name with the same value as what is given in the call to the factory function. I add it to the expectation in test_person.py

    11    def test_takes_keyword_arguments(self):
    12        first_name = 'jane'
    13
    14        self.assertEqual(
    15            src.person.factory(
    16                first_name=first_name,
    17                last_name='last_name',
    18                sex='M',
    19                year_of_birth=this_year()
    20            ),
    21            dict(
    22                first_name=first_name,
    23                last_name='last_name',
    24            )
    25        )
    

    the terminal shows AssertionError

    AssertionError: {'first_name': 'jane'} != {'first_name': 'jane', 'last_name': 'last_name'}
    
  • I change the return statement in person.py

    1def factory(
    2        first_name, last_name,
    3        sex, year_of_birth,
    4    ):
    5    return {
    6        'first_name': 'jane',
    7        'last_name': 'last_name',
    8    }
    

    the test passes

  • 'last_name' happens twice in the test, I add a variable to remove the duplication like I did with first_name in test_person.py

    11    def test_takes_keyword_arguments(self):
    12        first_name = 'jane'
    13        last_name = 'last_name'
    14
    15        self.assertEqual(
    16            src.person.factory(
    17                first_name=first_name,
    18                last_name=last_name,
    19                sex='M',
    20                year_of_birth=this_year()
    21            ),
    22            dict(
    23                first_name=first_name,
    24                last_name=last_name,
    25            )
    26        )
    

    I change the value

    Note

    the …(ellipsis) represents code that does not need to change in this part

    11    def test_takes_keyword_arguments(self):
    12        first_name = 'jane'
    13        last_name = 'doe'
    14
    15        ...
    

    the terminal shows AssertionError

    AssertionError: {'first_name': 'jane', 'last_name': 'last_name'} != {'first_name': 'jane', 'last_name': 'doe'}
    
  • I change the return statement in person.py

    1def factory(
    2        first_name, last_name,
    3        sex, year_of_birth,
    4    ):
    5    return {
    6        'first_name': 'jane',
    7        'last_name': 'doe',
    8    }
    

    the test passes

  • I add a key named sex to the dictionary with the same value as what is given in the call to the factory function in test_person.py

    11    def test_takes_keyword_arguments(self):
    12        first_name = 'jane'
    13        last_name = 'doe'
    14
    15        self.assertEqual(
    16            src.person.factory(
    17                first_name=first_name,
    18                last_name=last_name,
    19                sex='M',
    20                year_of_birth=this_year(),
    21            ),
    22            dict(
    23                first_name=first_name,
    24                last_name=last_name,
    25                sex='M',
    26            )
    27        )
    

    the terminal shows AssertionError

    AssertionError: {'first_name': 'jane', 'last_name': 'doe'} != {'first_name': 'jane', 'last_name': 'doe', 'sex': 'M'}
    

    I change the return statement in person.py

    1def factory(
    2        first_name, last_name,
    3        sex, year_of_birth,
    4    ):
    5    return {
    6        'first_name': 'jane',
    7        'last_name': 'doe',
    8        'sex': 'M',
    9    }
    

    the terminal shows green again

  • I add a variable to remove the repetition in test_person.py

    11    def test_takes_keyword_arguments(self):
    12        first_name = 'jane'
    13        last_name = 'doe'
    14        sex = 'M'
    15
    16        self.assertEqual(
    17            src.person.factory(
    18                first_name=first_name,
    19                last_name=last_name,
    20                sex=sex,
    21                year_of_birth=this_year(),
    22            ),
    23            dict(
    24                first_name=first_name,
    25                last_name=last_name,
    26                sex=sex,
    27            )
    28        )
    

    the test is still green

  • when I change the value of the sex variable

    Note

    the …(ellipsis) represents code that does not need to change in this part

    1    def test_takes_keyword_arguments(self):
    2        first_name = 'jane'
    3        last_name = 'doe'
    4        sex = 'F'
    5
    6        ...
    

    the terminal shows AssertionError

    AssertionError: {'first_name': 'jane', 'last_name': 'doe', 'sex': 'M'} != {'first_name': 'jane', 'last_name': 'doe', 'sex': 'F'}
    

    I change the return statement in person.py

    1def factory(
    2        first_name, last_name,
    3        sex, year_of_birth,
    4    ):
    5    return {
    6        'first_name': 'jane',
    7        'last_name': 'doe',
    8        'sex': 'F',
    9    }
    

    and the test is green again

  • I add a calculation and a key for age to the expectation in test_person.py

    11    def test_takes_keyword_arguments(self):
    12        first_name = 'jane'
    13        last_name = 'doe'
    14        sex = 'F'
    15
    16        self.assertEqual(
    17            src.person.factory(
    18                first_name=first_name,
    19                last_name=last_name,
    20                sex=sex,
    21                year_of_birth=this_year(),
    22            ),
    23            dict(
    24                first_name=first_name,
    25                last_name=last_name,
    26                sex=sex,
    27                age=this_year()-this_year(),
    28            )
    29        )
    

    the terminal shows TypeError

    TypeError: unsupported operand type(s) for -: 'NoneType' and 'NoneType'
    

    I cannot do subtraction with None and I want the value for the current year

  • I add an import statement at the top of test_person.py

    1import datetime
    2import src.person
    3import unittest
    4
    5
    6def this_year():
    7    return None
    

    datetime is a module from the python standard library that is used for dates and times

  • I change the return statement in the this_year function

    6def this_year():
    7    return datetime.datetime.now().year
    

    datetime.datetime.now().year returns the year attribute of the datetime object returned by the now method of the datetime class, from the datetime module. If you are confused, do not worry, that was a lot of terminology, it is explained in more detail when I test AttributeError and classes

    I can also use the today method to get the same value

    6def this_year():
    7    return datetime.datetime.today().year
    

    the terminal shows AssertionError

    AssertionError: {'first_name': 'jane', 'last_name': 'doe', 'sex': 'F'} != {'first_name': 'jane', 'last_name': 'doe', 'sex': 'F', 'age': 0}
    

    the new dictionary has a value for age

  • I change the return statement in person.py

     4def factory(
     5        first_name, last_name,
     6        sex, year_of_birth,
     7    ):
     8    return {
     9        'first_name': 'jane',
    10        'last_name': 'doe',
    11        'sex': 'F',
    12        'age': 0,
    13    }
    

    the test passes

  • I add a variable to remove duplication from test_person.py

    12    def test_takes_keyword_arguments(self):
    13        first_name = 'jane'
    14        last_name = 'doe'
    15        sex = 'F'
    16        year_of_birth = this_year()
    17
    18        self.assertEqual(
    19            src.person.factory(
    20                first_name=first_name,
    21                last_name=last_name,
    22                sex=sex,
    23                year_of_birth=year_of_birth,
    24            ),
    25            dict(
    26                first_name=first_name,
    27                last_name=last_name,
    28                sex=sex,
    29                age=this_year()-year_of_birth,
    30            )
    31        )
    

    and the test is still green

refactor: make it better

  • I add an import statement at the top of test_person.py to use random values in the tests

    Tip

    I like to arrange import statements alphabetically

    4import datetime
    5import random
    6import src.person
    7import unittest
    

    random is a module from the python standard library that is used to make fake random numbers. I use it for the year_of_birth variable

    13    def test_takes_keyword_arguments(self):
    14        first_name = 'jane'
    15        last_name = 'doe'
    16        sex = 'F'
    17        year_of_birth = random.randint(
    18            this_year()-120, this_year()
    19        )
    20
    21        ...
    

    random.randint(this_year()-120, this_year()) gives me a random number from 120 years ago, up to and including the current year which is returned by this_year(). I hit save ctrl+s (windows/linux) or command+s (mac) a few times to run the tests and and when the age is not 0, the terminal shows AssertionError

    AssertionError: {'first_name': 'jane', 'last_name': 'doe', 'sex': 'F', 'age': 0} != {'first_name': 'jane', 'last_name': 'doe', 'sex': 'F', 'age': X}
    

    I add the age calculation from test_person.py to the return statement in person.py

     4def factory(
     5        first_name, last_name,
     6        sex, year_of_birth,
     7    ):
     8    return {
     9        'first_name': 'jane',
    10        'last_name': 'doe',
    11        'sex': 'F',
    12        'age': this_year() - year_of_birth,
    13    }
    

    the terminal shows NameError

    NameError: name 'this_year' is not defined
    

    because I called a function that is NOT in person.py.

  • I use the return statement of the this_year() function from test_person.py to change this_year() in person.py

     4def factory(
     5        first_name, last_name,
     6        sex, year_of_birth,
     7    ):
     8    return {
     9        'first_name': 'jane',
    10        'last_name': 'doe',
    11        'sex': 'F,
    12        'age': datetime.datetime.now().year - year_of_birth,
    13    }
    

    the terminal shows NameError

    NameError: name 'datetime' is not defined. Did you forget to import 'datetime'
    

    I add an import statement at the top of person.py

    Note

    the …(ellipsis) represents code that does not need to change in this part

    1import datetime
    2
    3
    4def factory(
    5    ...
    

    the test passes

  • I add randomness to the sex variable in test_person.py

    13    def test_takes_keyword_arguments(self):
    14        first_name = 'jane'
    15        last_name = 'doe'
    16        sex = random.choice(('F', 'M'))
    17        year_of_birth = random.randint(
    18            this_year()-120, this_year()
    19        )
    20
    21        ...
    

    random.choice(('F', 'M')) randomly gives me F or M every time the test runs. I hit save ctrl+s (windows/linux) or command+s (mac) a few times to run the tests and the terminal shows success when sex is randomly 'F', and when it is randomly 'M', the terminal shows AssertionError

    AssertionError: {'first_name': 'jane', 'last_name': 'doe', 'sex': 'F', 'age': X} != {'first_name': 'jane', 'last_name': 'doe', 'sex': 'M', 'age': X}
    

    I add the sex input parameter instead of a value that does not change to the return statement in person.py

     4def factory(
     5        first_name, last_name,
     6        sex, year_of_birth,
     7    ):
     8    return {
     9        'first_name': 'jane',
    10        'last_name': 'doe',
    11        'sex': sex,
    12        'age': datetime.datetime.now().year - year_of_birth,
    13    }
    

    the test passes with no more random failures

  • I use random.choice with the last_name variable in test_person.py

    13    def test_takes_keyword_arguments(self):
    14        first_name = 'jane'
    15        last_name = random.choice((
    16            'doe', 'smith', 'blow', 'public',
    17        ))
    18        sex = random.choice(('F', 'M'))
    19        year_of_birth = random.randint(
    20            this_year()-120, this_year()
    21        )
    22
    23        ...
    

    I hit save ctrl+s (windows/linux) or command+s (mac) a few times to run the tests and the terminal shows success when last_name is 'doe', and when it is not, the terminal shows AssertionError

    AssertionError: {'first_name': 'jane', 'last_name': 'doe', 'sex': X, 'age': Y} != {'first_name': 'jane', 'last_name': 'public', 'sex': X, 'age': Y}
    AssertionError: {'first_name': 'jane', 'last_name': 'doe', 'sex': X, 'age': Y} != {'first_name': 'jane', 'last_name': 'smith', 'sex': X, 'age': Y}
    AssertionError: {'first_name': 'jane', 'last_name': 'doe', 'sex': X, 'age': Y} != {'first_name': 'jane', 'last_name': 'blow', 'sex': X, 'age': Y}
    

    I add the last_name input parameter in the return statement in person.py

     4def factory(
     5        first_name, last_name,
     6        sex, year_of_birth,
     7    ):
     8    return {
     9        'first_name': 'jane',
    10        'last_name': last_name,
    11        'sex': sex,
    12        'age': datetime.datetime.today().year - year_of_birth,
    13    }
    

    and the test is green again

  • I do the same thing for the first_name variable in test_person.py

    13    def test_takes_keyword_arguments(self):
    14        first_name = random.choice((
    15            'jane', 'joe', 'john', 'person',
    16        ))
    17        last_name = random.choice((
    18            'doe', 'smith', 'blow', 'public',
    19        ))
    20        sex = random.choice(('F', 'M'))
    21        year_of_birth = random.randint(
    22            this_year()-120, this_year()
    23        )
    24
    25        ...
    

    I hit save ctrl+s (windows/linux) or command+s (mac) a few times to run the tests and the terminal shows green when first_name is 'jane', and when it is not, the terminal shows AssertionError

    AssertionError: {'first_name': 'jane', 'last_name': X, 'sex': Y, 'age': Z} != {'first_name': 'joe', 'last_name': X, 'sex': Y, 'age': Z}
    AssertionError: {'first_name': 'jane', 'last_name': X, 'sex': Y, 'age': Z} != {'first_name': 'john', 'last_name': X, 'sex': Y, 'age': Z}
    AssertionError: {'first_name': 'jane', 'last_name': X, 'sex': Y, 'age': Z} != {'first_name': 'person', 'last_name': X, 'sex': Y, 'age': Z}
    

    I add the first_name input parameter in the return statement in person.py

     4def factory(
     5        first_name, last_name,
     6        sex, year_of_birth,
     7    ):
     8    return {
     9        'first_name': first_name,
    10        'last_name': last_name,
    11        'sex': sex,
    12        'age': datetime.datetime.today().year - year_of_birth,
    13    }
    

    the test passes!


test_function_w_default_keyword_arguments

I want to see what would happen when I try to make a person without a value for the last_name argument

red: make it fail

  • I select test_takes_keyword_arguments, then copy (ctrl+c (windows/linux) or command+c (mac)) and paste (ctrl+v (windows/linux) or command+v (mac)) it below in test_person.py

  • I change the name of the new test to test_function_w_default_keyword_arguments and comment out the last_name variable

     1import datetime
     2import random
     3import src.person
     4import unittest
     5
     6
     7def this_year():
     8    return datetime.datetime.now().year
     9
    10
    11class TestPerson(unittest.TestCase):
    12
    13    def test_takes_keyword_arguments(self):
    14        first_name = random.choice((
    15            'jane', 'joe', 'john', 'person',
    16        ))
    17        last_name = random.choice((
    18            'doe', 'smith', 'blow', 'public',
    19        ))
    20        sex = random.choice(('F', 'M'))
    21        year_of_birth = random.randint(
    22            this_year()-120, this_year()
    23        )
    24
    25        self.assertEqual(
    26            src.person.factory(
    27                first_name=first_name,
    28                last_name=last_name,
    29                sex=sex,
    30                year_of_birth=year_of_birth,
    31            ),
    32            dict(
    33                first_name=first_name,
    34                last_name=last_name,
    35                sex=sex,
    36                age=this_year()-year_of_birth,
    37            )
    38        )
    39
    40    def test_function_w_default_keyword_arguments(self):
    41        first_name = random.choice((
    42            'jane', 'joe', 'john', 'person',
    43        ))
    44        # last_name = random.choice((
    45        #    'doe', 'smith', 'blow', 'public',
    46        # ))
    47        sex = random.choice(('F', 'M'))
    48        year_of_birth = random.randint(
    49            this_year()-120, this_year()
    50        )
    51
    52        self.assertEqual(
    53            src.person.factory(
    54                first_name=first_name,
    55                last_name=last_name,
    56                sex=sex,
    57                year_of_birth=year_of_birth,
    58            ),
    59            dict(
    60                first_name=first_name,
    61                last_name=last_name,
    62                sex=sex,
    63                age=this_year()-year_of_birth,
    64            )
    65        )
    

    the terminal shows NameError

    NameError: name 'last_name' is not defined
    

green: make it pass

  • I comment out last_name in the call to the factory function in test_function_w_default_keyword_arguments

    52        self.assertEqual(
    53            src.person.factory(
    54                first_name=first_name,
    55                # last_name=last_name,
    56                year_of_birth=year_of_birth,
    57                sex=sex,
    58            ),
    59            dict(
    60                first_name=first_name,
    61                last_name=last_name,
    62                sex=sex,
    63                age=this_year() - year_of_birth,
    64            )
    65        )
    

    the terminal shows TypeError

    TypeError: factory() missing 1 required positional argument: 'last_name'
    

    the factory function is called with 3 arguments in test_function_w_default_keyword_arguments but the definition expects 4 in person.py

  • I add a default value for last_name in person.py

    4def factory(
    5        first_name, last_name=None,
    6        sex, year_of_birth,
    7    ):
    8    ...
    

    the terminal shows SyntaxError

    SyntaxError: parameter without a default follows parameter with a default
    
  • I add it to the list of Exceptions encountered in test_person.py

    68# Exceptions Encountered
    69# AssertionError
    70# NameError
    71# AttributeError
    72# TypeError
    73# SyntaxError
    
  • I add a default value for the sex parameter in the factory function in person.py

    4def factory(
    5        first_name, last_name=None,
    6        sex=None, year_of_birth,
    7    ):
    8    ...
    

    the terminal shows SyntaxError

    SyntaxError: parameter without a default follows parameter with a default
    
  • I give the year_of_birth parameter a default value as well

    4def factory(
    5        first_name, last_name=None,
    6        sex=None, year_of_birth=None,
    7    ):
    8    ...
    

    the terminal shows NameError

    NameError: name 'last_name' is not defined
    

    the value for the last_name key in the expected dictionary in test_function_w_default_keyword_arguments points to the last_name variable which I just commented out

  • I change the expectation of test_function_w_default_keyword_arguments in test_person.py

    52        self.assertEqual(
    53            src.person.factory(
    54                first_name=first_name,
    55                # last_name=last_name,
    56                sex=sex,
    57                year_of_birth=year_of_birth,
    58            ),
    59            dict(
    60                first_name=first_name,
    61                last_name='doe',
    62                sex=sex,
    63                age=this_year()-year_of_birth,
    64            )
    65        )
    

    the terminal shows AssertionError

    AssertionError: {'first_name': X, 'last_name': None, 'sex': Y, 'age': Z} != {'first_name': X, 'last_name': 'doe', 'sex': Y, 'age': Z}
    

    the factory function returns a dictionary with a value of None for last_name and the test expects 'doe'

  • I change the default value for last_name in the factory function in person.py

    4def factory(
    5        first_name, last_name='doe',
    6        sex=None, year_of_birth=None,
    7    ):
    8    ...
    

    the test passes

    When the factory function is called with no value for the last_name argument, it uses 'doe' because that is the default value in the function definition, it is the same as calling it with last_name='doe'

    src.person.factory(
        first_name=first_name,
        sex=sex,
        year_of_birth=year_of_birth,
        last_name='doe',
    )
    
  • I remove the commented lines from test_function_w_default_keyword_arguments in test_person.py

    40    def test_function_w_default_keyword_arguments(self):
    41        first_name = random.choice((
    42            'jane', 'joe', 'john', 'person',
    43        ))
    44        sex = random.choice(('F', 'M'))
    45        year_of_birth = random.randint(
    46            this_year()-120, this_year()
    47        )
    48
    49        self.assertEqual(
    50            src.person.factory(
    51                first_name=first_name,
    52                sex=sex,
    53                year_of_birth=year_of_birth,
    54            ),
    55            dict(
    56                first_name=first_name,
    57                last_name='doe',
    58                sex=sex,
    59                age=this_year()-year_of_birth,
    60            )
    61        )
    
  • I comment out the sex variable in the test to see what would happen if I do not know its value

    40    def test_function_w_default_keyword_arguments(self):
    41        first_name = random.choice((
    42            'jane', 'joe', 'john', 'person',
    43        ))
    44        # sex = random.choice(('F', 'M'))
    45        year_of_birth = random.randint(
    46            this_year()-120, this_year()
    47        )
    

    the terminal shows NameError

    NameError: name 'sex' is not defined
    
  • I comment out sex in the call to the factory function in the assertion in test_person.py

    49      self.assertEqual(
    50          src.person.factory(
    51              first_name=first_name,
    52              # sex=sex,
    53              year_of_birth=year_of_birth,
    54          ),
    55          dict(
    56              first_name=first_name,
    57              last_name='doe',
    58              sex=sex,
    59              age=this_year()-year_of_birth,
    60          )
    61      )
    

    the terminal shows NameError

    NameError: name 'sex' is not defined
    

    the value in the expected dictionary still uses the sex variable I commented out. I change the expectation

    49    self.assertEqual(
    50        src.person.factory(
    51            first_name=first_name,
    52            # sex=sex,
    53            year_of_birth=year_of_birth,
    54        ),
    55        dict(
    56            first_name=first_name,
    57            last_name='doe',
    58            sex='M',
    59            age=this_year()-year_of_birth,
    60        )
    61    )
    

    the terminal shows AssertionError

    AssertionError: {'first_name': X, 'last_name': 'doe', 'sex': None, 'age': Y} != {'first_name': X, 'last_name': 'doe', 'sex': 'M', 'age': Y}
    

    the factory function returns a dictionary with None as the value for sex and the test expects 'M'

  • I add a default value for sex in person.py

    4def factory(
    5        first_name, last_name='doe',
    6        sex='M', year_of_birth=None
    7    ):
    8    ...
    

    the test passes

    When the factory function is called with no value for the sex argument, it uses 'M' because that is the default value in the function definition, it is the same as calling it with sex='M'

    src.person.factory(
        first_name=first_name,
        year_of_birth=year_of_birth,
        last_name='doe',
        sex='M',
    )
    

    since the values are the same as the default values, I can call the function without them

    src.person.factory(
        first_name=first_name,
        year_of_birth=year_of_birth,
    )
    
  • I remove the commented lines from test_person.py

    40    def test_function_w_default_keyword_arguments(self):
    41        first_name = random.choice((
    42            'jane', 'joe', 'john', 'person',
    43        ))
    44        year_of_birth = random.randint(
    45            this_year()-120, this_year()
    46        )
    47
    48        self.assertEqual(
    49            src.person.factory(
    50                first_name=first_name,
    51                year_of_birth=year_of_birth,
    52            ),
    53            dict(
    54                first_name=first_name,
    55                last_name='doe',
    56                sex='M',
    57                age=this_year()-year_of_birth,
    58            )
    59        )
    

    the terminal still shows green

refactor: make it better

  • first_name and year_of_birth are made the same way in both tests, I can remove this repetition by adding attributes to the TestPerson class

    Note

    the …(ellipsis) represents code that does not need to change in this part

     1import datetime
     2import random
     3import src.person
     4import unittest
     5
     6
     7def this_year():
     8    return datetime.datetime.now().year
     9
    10
    11class TestPerson(unittest.TestCase):
    12
    13    first_name = random.choice((
    14        'jane', 'joe', 'john', 'person',
    15    ))
    16    year_of_birth = random.randint(
    17        this_year()-120, this_year()
    18    )
    19
    20    def test_takes_keyword_arguments(self):
    21        ...
    

    I can use them in test_takes_keyword_arguments with self. the same way I use the assert methods since they now belong to the TestPerson class

    20    def test_takes_keyword_arguments(self):
    21        # first_name = random.choice((
    22        #    'jane', 'joe', 'john', 'person',
    23        # ))
    24        first_name = self.first_name
    25        last_name = random.choice((
    26            'doe', 'smith', 'blow', 'public',
    27        ))
    28        sex = random.choice(('F', 'M'))
    29        # year_of_birth = random.randint(
    30        #    this_year()-120, this_year()
    31        # )
    32        year_of_birth = self.year_of_birth
    33
    34        self.assertEqual(
    35            ...
    

    the test is still green. I do the same thing in test_function_w_default_keyword_arguments

    49    def test_function_w_default_keyword_arguments(self):
    50        # first_name = random.choice((
    51        #    'jane', 'joe', 'john', 'person',
    52        # ))
    53        first_name = self.first_name
    54        # year_of_birth = random.randint(
    55        #    this_year()-120, this_year()
    56        # )
    57        year_of_birth = self.year_of_birth
    58
    59        self.assertEqual(
    60            ...
    

    the terminal still shows green

  • I remove the commented lines from test_takes_keyword_arguments

    20    def test_takes_keyword_arguments(self):
    21        first_name = self.first_name
    22        last_name = random.choice((
    23            'doe', 'smith', 'blow', 'public',
    24        ))
    25        sex = random.choice(('F', 'M'))
    26        year_of_birth = self.year_of_birth
    27
    28        self.assertEqual(
    29            ...
    

    and remove the commented lines from test_function_w_default_keyword_arguments

    43    def test_function_w_default_keyword_arguments(self):
    44        first_name = self.first_name
    45        year_of_birth = self.year_of_birth
    46
    47        self.assertEqual(
    48            ...
    

    still green

  • since the variables point to class attributes, I can use them directly and comment out first_name and year_of_birth in test_takes_keyword_arguments

    20    def test_takes_keyword_arguments(self):
    21        # first_name = self.first_name
    22        last_name = random.choice((
    23            'doe', 'smith', 'blow', 'public',
    24        ))
    25        sex = random.choice(('F', 'M'))
    26        # year_of_birth = self.year_of_birth
    27
    28        self.assertEqual(
    29            src.person.factory(
    30                first_name=self.first_name,
    31                last_name=last_name,
    32                sex=sex,
    33                year_of_birth=self.year_of_birth,
    34            ),
    35            dict(
    36                first_name=self.first_name,
    37                last_name=last_name,
    38                sex=sex,
    39                age=this_year()-self.year_of_birth,
    40            )
    41        )
    

    the test is still green. I make the same change in test_function_w_default_keyword_arguments

    43    def test_function_w_default_keyword_arguments(self):
    44        # first_name = self.first_name
    45        # year_of_birth = self.year_of_birth
    46
    47        self.assertEqual(
    48            src.person.factory(
    49                first_name=self.first_name,
    50                year_of_birth=self.year_of_birth,
    51            ),
    52            dict(
    53                first_name=self.first_name,
    54                last_name='doe',
    55                sex='M',
    56                age=this_year()-self.year_of_birth,
    57            )
    58        )
    

    all tests are still passing

  • I remove the commented lines from test_takes_keyword_arguments

    20    def test_takes_keyword_arguments(self):
    21        last_name = random.choice((
    22            'doe', 'smith', 'blow', 'public',
    23        ))
    24        sex = random.choice(('F', 'M'))
    25
    26        self.assertEqual(
    27            ...
    

    and in test_function_w_default_keyword_arguments

    41    def test_function_w_default_keyword_arguments(self):
    42        self.assertEqual(
    43            src.person.factory(
    44                first_name=self.first_name,
    45                year_of_birth=self.year_of_birth
    46            ),
    47            dict(
    48                first_name=self.first_name,
    49                last_name='doe',
    50                sex='M',
    51                age=this_year()-self.year_of_birth,
    52            )
    53        )
    

    the tests are still passing

  • both tests have the same random values for first_name and year_of_birth, they were not always the same before the change. I add the unittest.TestCase.setUp method to make sure they both get new random values before each test runs

    13class TestPerson(unittest.TestCase):
    14
    15    def setUp(self):
    16        first_name = random.choice((
    17            'jane', 'joe', 'john', 'person',
    18        ))
    19        year_of_birth = random.randint(
    20            this_year()-120, this_year()
    21        )
    22
    23    def test_takes_keyword_arguments(self):
    24        ...
    

    the terminal shows AttributeError

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

    because there is no longer a class attribute named first_name, it is now a variable in the unittest.TestCase.setUp method and the other methods cannot reach it

  • I add self. to make it a class attribute

    13    def setUp(self):
    14        self.first_name = random.choice((
    15            'jane', 'joe', 'john', 'person',
    16        ))
    17        year_of_birth = random.randint(
    18            this_year()-120, this_year()
    19        )
    20
    21    def test_takes_keyword_arguments(self):
    22        ...
    

    the terminal shows AttributeError

    AttributeError: 'TestPerson' object has no attribute 'year_of_birth'
    
  • same problem, same solution

    13    def setUp(self):
    14        self.first_name = random.choice((
    15            'jane', 'joe', 'john', 'person',
    16        ))
    17        self.year_of_birth = random.randint(
    18            this_year()-120, this_year()
    19        )
    20
    21    def test_takes_keyword_arguments(self):
    22        ...
    

    and both tests are green again!

    self.first_name and self.year_of_birth are given random values before the first test, then given random values again before the second test. That was a lot, but we got through it.


test_person_tests

red: make it fail

  • I close test_person.py

  • I want to write the solution without looking at the tests and delete all the text in person.py. The terminal shows AttributeError

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

green: make it pass

  • I add the name

    1factory
    

    the terminal shows NameError

    NameError: name 'factory' is not defined
    
  • I point it to None

    1factory = None
    

    the terminal shows TypeError

    TypeError: 'NoneType' object is not callable
    
  • when I make it a function

    1def factory():
    2    return None
    

    the terminal shows TypeError

    TypeError: factory() got an unexpected keyword argument 'first_name'
    

    I add the keyword argument to the function definition

    1def factory(first_name):
    2    return None
    

    the terminal shows TypeError

    TypeError: factory() got an unexpected keyword argument 'last_name'
    

    I add the keyword argument

    1def factory(first_name, last_name):
    2    return None
    

    the terminal shows TypeError

    TypeError: factory() got an unexpected keyword argument 'sex'
    

    when I add the keyword argument

    1def factory(
    2        first_name, last_name,
    3        sex
    4    ):
    5    return None
    

    the terminal shows TypeError

    TypeError: factory() got an unexpected keyword argument 'year_of_birth'
    

    I add the missing keyword argument

    1def factory(
    2        first_name, last_name,
    3        sex, year_of_birth,
    4    ):
    5    return None
    

    the terminal shows AssertionError

    AssertionError: None != {'first_name': X, 'last_name': Y, 'sex': Z, 'age': A}
    
  • I change the return statement

     1def factory(
     2        first_name, last_name,
     3        sex, year_of_birth,
     4    ):
     5    return {
     6        'first_name': 'john',
     7        'last_name': 'blow',
     8        'sex': 'F',
     9        'age': 20,
    10    }
    

    the terminal shows AssertionError

    AssertionError: {'first_name': 'john', 'last_name': 'blow', 'sex': 'F', 'age': 20} != {'first_name': 'jane', 'last_name': 'smith', 'sex': 'M', 'age': 50}
    AssertionError: {'first_name': 'john', 'last_name': 'blow', 'sex': 'F', 'age': 20} != {'first_name': 'jane', 'last_name': 'doe', 'sex': 'M', 'age': 73}
    AssertionError: {'first_name': 'john', 'last_name': 'blow', 'sex': 'F', 'age': 20} != {'first_name': 'john', 'last_name': 'smith', 'sex': 'M', 'age': 77}
    AssertionError: {'first_name': 'john', 'last_name': 'blow', 'sex': 'F', 'age': 20} != {'first_name': 'jane', 'last_name': 'public', 'sex': 'M', 'age': 98}
    

    the values of first_name, last_name, sex and age change every time the test runs

  • I make the dictionary in the return statement use the first_name input parameter instead of a value that does not change

     1def factory(
     2        first_name, last_name,
     3        sex, year_of_birth,
     4    ):
     5    return {
     6        'first_name': first_name,
     7        'last_name': 'blow',
     8        'sex': 'F',
     9        'age': 20,
    10    }
    

    the terminal shows AssertionError

    AssertionError: {'first_name': X, 'last_name': 'blow', 'sex': 'F', 'age': 20} != {'first_name': X, 'last_name': 'public', 'sex': 'M', 'age': 69}
    AssertionError: {'first_name': X, 'last_name': 'blow', 'sex': 'F', 'age': 20} != {'first_name': X, 'last_name': 'blow', 'sex': 'M', 'age': 97}
    AssertionError: {'first_name': X, 'last_name': 'blow', 'sex': 'F', 'age': 20} != {'first_name': X, 'last_name': 'smith', 'sex': 'M', 'age': 74}
    AssertionError: {'first_name': X, 'last_name': 'blow', 'sex': 'F', 'age': 20} != {'first_name': X, 'last_name': 'public', 'sex': 'M', 'age': 19}
    

    the first_name now matches, and the values of last_name, sex and age change every time the test runs

  • I use the last_name input parameter in the return statement

     1def factory(
     2        first_name, last_name,
     3        sex, year_of_birth,
     4    ):
     5    return {
     6        'first_name': first_name,
     7        'last_name': last_name,
     8        'sex': 'F',
     9        'age': 20,
    10    }
    

    the terminal shows AssertionError

    AssertionError: {'first_name': X, 'last_name': Y, 'sex': 'F', 'age': 20} != {'first_name': X, 'last_name': Y, 'sex': 'M', 'age': 3}
    AssertionError: {'first_name': X, 'last_name': Y, 'sex': 'F', 'age': 20} != {'first_name': X, 'last_name': Y, 'sex': 'M', 'age': 118}
    AssertionError: {'first_name': X, 'last_name': Y, 'sex': 'F', 'age': 20} != {'first_name': X, 'last_name': Y, 'sex': 'M', 'age': 19}
    AssertionError: {'first_name': X, 'last_name': Y, 'sex': 'F', 'age': 20} != {'first_name': X, 'last_name': Y, 'sex': 'M', 'age': 95}
    

    the values for first_name, and last_name now match, the values of sex and age change every time the test runs

  • When I add the sex input parameter to the return statement

     1def factory(
     2        first_name, last_name,
     3        sex, year_of_birth,
     4    ):
     5    return {
     6        'first_name': first_name,
     7        'last_name': last_name,
     8        'sex': sex,
     9        'age': 20,
    10    }
    

    the terminal shows AssertionError

    AssertionError: {'first_name': X, 'last_name': Y, 'sex': Z, 'age': 20} != {'first_name': X, 'last_name': Y, 'sex': Z, 'age': 1}
    AssertionError: {'first_name': X, 'last_name': Y, 'sex': Z, 'age': 20} != {'first_name': X, 'last_name': Y, 'sex': Z, 'age': 90}
    AssertionError: {'first_name': X, 'last_name': Y, 'sex': Z, 'age': 20} != {'first_name': X, 'last_name': Y, 'sex': Z, 'age': 113}
    AssertionError: {'first_name': X, 'last_name': Y, 'sex': Z, 'age': 20} != {'first_name': X, 'last_name': Y, 'sex': Z, 'age': 58}
    

    the first_name, last_name and sex match, the value of age changes every time the test runs

  • I add the year_of_birth input parameter to the return statement

     1def factory(
     2        first_name, last_name,
     3        sex, year_of_birth,
     4    ):
     5    return {
     6        'first_name': first_name,
     7        'last_name': last_name,
     8        'sex': sex,
     9        'age': year_of_birth,
    10    }
    

    the terminal shows AssertionError

    AssertionError: {'first_name': X, 'last_name': Y, 'sex': Z, 'age': 2022} != {'first_name': X, 'last_name': Y, 'sex': Z, 'age': 2}
    AssertionError: {'first_name': X, 'last_name': Y, 'sex': Z, 'age': 2024} != {'first_name': X, 'last_name': Y, 'sex': Z, 'age': 0}
    AssertionError: {'first_name': X, 'last_name': Y, 'sex': Z, 'age': 1981} != {'first_name': X, 'last_name': Y, 'sex': Z, 'age': 43}
    AssertionError: {'first_name': X, 'last_name': Y, 'sex': Z, 'age': 1969} != {'first_name': X, 'last_name': Y, 'sex': Z, 'age': 55}
    

    the test expects the difference between the current year and the year_of_birth to get the age

  • I add an import statement at the top of the file

    1import datetime
    2
    3
    4def factory(
    5    ...
    

    then I use it to get the current year for the age calculation

     4def factory(
     5        first_name, last_name,
     6        sex, year_of_birth,
     7    ):
     8    return {
     9        'first_name': first_name,
    10        'last_name': last_name,
    11        'sex': sex,
    12        'age': datetime.datetime.today().year - year_of_birth,
    13    }
    

    the terminal shows TypeError

    TypeError: factory() missing 2 required positional arguments: 'last_name' and 'sex'
    
  • I add a default value for last_name

    4def factory(
    5        first_name, last_name=None,
    6        sex, year_of_birth,
    7    ):
    8    ...
    

    the terminal shows SyntaxError

    SyntaxError: parameter without a default follows parameter with a default
    
  • I add a default value for sex

    4def factory(
    5        first_name, last_name=None,
    6        sex=None, year_of_birth,
    7    ):
    8    ...
    

    the terminal shows SyntaxError

    SyntaxError: parameter without a default follows parameter with a default
    
  • I add a default value for year_of_birth

    4def factory(
    5        first_name, last_name=None,
    6        sex=None, year_of_birth=None,
    7    ):
    

    the terminal shows AssertionError

    AssertionError: {'first_name': X, 'last_name': None, 'sex': None, 'age': Y} != {'first_name': X, 'last_name': 'doe', 'sex': 'M', 'age': Y}
    

    the values for last_name and sex do not match the expectation

  • I change the default value for last_name to match

    4def factory(
    5        first_name, last_name='doe',
    6        sex=None, year_of_birth=None,
    7    ):
    8    ...
    

    the terminal shows AssertionError

    AssertionError: {'first_name': X, 'last_name': Y, 'sex': None, 'age': Z} != {'first_name': Z, 'last_name': Y, 'sex': 'M', 'age': Z}
    
  • when I make the default value of the sex argument match the expectation

     1import datetime
     2
     3
     4def factory(
     5        first_name, last_name='doe',
     6        sex='M', year_of_birth=None
     7    ):
     8    return {
     9        'first_name': first_name,
    10        'last_name': last_name,
    11        'sex': sex,
    12        'age': datetime.datetime.today().year - year_of_birth,
    13    }
    

    both tests pass! I think I am pretty good at this

review

I ran tests to make a function that takes in keyword arguments as input, has default values for some of them, performs an action based on an input and returns a dictionary as output

I also ran into the following Exceptions

Would you like to know how to test that an Exception is raised?


how to make a person: tests and solution