how to make a calculator 1


preview

These are the tests I have by the end of the chapter


start the project

  • I name this project calculator

  • I open a terminal

  • I make a directory for the project

    mkdir calculator
    

    the terminal goes back to the command line

  • I change directory to the project

    cd calculator
    

    the terminal shows I am in the calculator folder

    .../pumping_python/calculator
    
  • I make a directory for the source code

    mkdir src
    

    the terminal goes back to the command line

  • I make a Python file to hold the source code in the src directory

    touch src/calculator.py
    

    Note

    on Windows without Windows Subsystem for Linux use New-Item src/calculator.py instead of touch src/calculator.py

    New-Item src/calculator.py
    

    the terminal goes back to the command line

  • I make a directory for the tests

    mkdir tests
    

    the terminal goes back to the command line

  • I make the tests directory a Python package

    Danger

    use 2 underscores (__) before and after init for __init__.py not _init_.py

    touch tests/__init__.py
    

    Note

    on Windows without Windows Subsystem for Linux use New-Item tests/__init__.py instead of touch tests/__init__.py

    New-Item tests/__init__.py
    

    the terminal goes back to the command line

  • I make a Python file for the tests in the tests directory

    touch tests/test_calculator.py
    

    Note

    on Windows without Windows Subsystem for Linux use New-Item tests/test_calculator.py instead of touch tests/test_calculator.py

    New-Item tests/test_calculator.py
    

    the terminal goes back to the command line

  • I open test_calculator.py in the editor of the Integrated Development Environment (IDE)

    Tip

    I can open a file from the terminal in the Integrated Development Environment (IDE) with the name of the program and the name of the file. That means if I type this in the terminal

    code tests/test_calculator.py
    

    Visual Studio Code opens test_calculator.py in the editor

  • I add the first failing test to test_calculator.py

    1import unittest
    2
    3
    4class TestCalculator(unittest.TestCase):
    5
    6    def test_failure(self):
    7        self.assertFalse(True)
    
  • I make a requirements file for the Python packages I need, in the terminal

    echo "pytest" > requirements.txt
    

    the terminal goes back to the command line

  • I add pytest-watcher to the file

    echo "pytest-watcher" >> requirements.txt
    

    the terminal goes back to the command line

  • I setup the project with uv

    uv init
    

    the terminal shows

    Initialized project `calculator`
    
  • I remove main.py from the project because I do not use it

    rm main.py
    

    the terminal goes back to the command line

  • I install the Python packages I gave in the requirements file

    uv add --requirement requirements.txt
    

    the terminal shows it installed the Python packages

  • I use pytest-watcher to run the tests automatically

    uv run pytest-watcher . --now
    

    the terminal shows

    ================================ FAILURES ================================
    _____________________ TestCalculator.test_failure ________________________
    
    self = <tests.test_calculator.TestCalculator testMethod=test_failure>
    
        def test_failure(self):
    >       self.assertFalse(True)
    E       AssertionError: True is not false
    
    tests/test_calculator.py:7: AssertionError
    ======================== short test summary info =========================
    FAILED tests/test_calculator.py::TestCalculator::test_failure - AssertionError: True is not false
    =========================== 1 failed in X.YZs ============================
    
  • I hold ctrl (Windows/Linux) or option/command (MacOS) on the keyboard and use the mouse to click on tests/test_calculator.py:7 to put the cursor on line 7 in the editor

  • I add AssertionError to the list of Exceptions seen in test_calculator.py

     4class TestCalculator(unittest.TestCase):
     5
     6    def test_failure(self):
     7        self.assertFalse(True)
     8
     9
    10# Exceptions seen
    11# AssertionError
    
  • then I change True to False in the assertion

    7        self.assertFalse(False)
    

    the test passes


  • I add a TODO list to keep track of the work for the program

     1import unittest
     2
     3
     4class TestCalculator(unittest.TestCase):
     5
     6    def test_failure(self):
     7        self.assertFalse(False)
     8
     9
    10# TODO
    11# test addition
    12# test subtraction
    13# test multiplication
    14# test division
    15
    16
    17# Exceptions seen
    18# AssertionError
    

test_addition


RED: make it fail


  • I change test_failure to test_addition then change assertFalse to assertEqual

    1import unittest
    2
    3
    4class TestCalculator(unittest.TestCase):
    5
    6    def test_addition(self):
    7        self.assertEqual(src.calculator.add(0, 1), 1)
    
    • the assertEqual method from AssertionError checks if the 2 things in parentheses are the same. It is like the statement assert x == y or asking is x equal to y?

    • I think of

      self.assertEqual(src.calculator.add(0, 1), 1)
      

      as

      self.assertEqual(reality, my_expectation)
      

      where

      • reality is src.calculator.add(0, 1)

      • my_expectation is 1 because 0 + 1 is 1

      in other words, self.assertEqual(src.calculator.add(0, 1), 1) checks if the result of calling src.calculator.add with 0 and 1 as input is equal to 1

    the terminal shows NameError

    NameError: name 'src' is not defined
    

    because I do not have a definition for src in test_calculator.py


GREEN: make it pass


  • I add NameError to the list of Exceptions seen in test_calculator.py

    17# Exceptions seen
    18# AssertionError
    19# NameError
    
  • I add an import statement at the top of the file for the calculator module

    1import src.calculator
    2import unittest
    3
    4
    5class TestCalculator(unittest.TestCase):
    

    the terminal shows AttributeError

    AttributeError: module 'src.calculator' has no attribute 'add'
    

    I think of src.calculator.add as an address, add is something (an attribute) in the empty calculator.py file from the src folder

  • I add AttributeError to the list of Exceptions seen in test_calculator.py

    18# Exceptions seen
    19# AssertionError
    20# NameError
    21# AttributeError
    
  • I open calculator.py from the src folder in the editor, then add the name

    1add
    

    the terminal shows NameError

    NameError: name 'add' is not defined
    

    I have to tell Python what the name add means

  • I point it to None to define it

    1add = None
    

    the terminal shows TypeError

    TypeError: 'NoneType' object is not callable
    

    because the add variable is now a name for None, and I cannot call None like a function

  • I add TypeError to the list of Exceptions seen in test_calculator.py

    18# Exceptions seen
    19# AssertionError
    20# NameError
    21# AttributeError
    22# TypeError
    
  • I use the def keyword in calculator.py to make add a function so it is callable

    1def add():
    2    return None
    

    the terminal shows TypeError

    TypeError: add() takes 0 positional arguments but 2 were given
    

    the definition of add does not allow it take input, but 2 were given in the call - 0 and 1

  • I add names in the parentheses to make the function take 2 inputs

    1def add(first_input, second_input):
    2    return None
    

    the terminal shows AssertionError

    AssertionError: None != 1
    

    the add function returns None, the test expects 1

  • I make the return statement give the test what it wants

    1def add(first_input, second_input):
    2    return 1
    

    the test passes, time for a victory lap!


REFACTOR: make it better


The add function passes the test but does not do what I actually want because it always returns 1. I want it to return the result of a calculation with the inputs

  • I add another assertion to show the problem with the function in test_calculator.py

    7    def test_addition(self):
    8        self.assertEqual(src.calculator.add(0, 1), 1)
    9        self.assertEqual(src.calculator.add(0, 2), 2)
    

    the terminal shows AssertionError

    AssertionError: 1 != 2
    

    the function returns 1, the test expects 2

  • I change the return statement in calculator.py

    1def add(first_input, second_input):
    2    return 2
    

    the terminal shows AssertionError

    AssertionError: 2 != 1
    

    this makes the assertion that was passing before - src.calculator.add(0, 1) fail. If the test sends

    • 0 and 2 to src.calculator.add it returns 2

    • 0 and 1 to src.calculator.add it returns 2

    I need a solution that can make the two tests pass. The function should return

    • 2 if the test sends 0 and 2 to src.calculator.add because 0 + 2 == 2

    • 1 if the test sends 0 and 1 to src.calculator.add because 0 + 1 == 1

  • I make the function return the result of adding the two inputs

    1def add(first_input, second_input):
    2    return first_input + second_input
    

    the two tests are passing

  • I add another test to make sure the function works for other numbers in test_calculator.py

     7    def test_addition(self):
     8        self.assertEqual(src.calculator.add(0, 1), 1)
     9        self.assertEqual(src.calculator.add(0, 2), 2)
    10        self.assertEqual(src.calculator.add(0, 3), 2)
    

    the terminal shows AssertionError

    AssertionError: 3 != 2
    
  • I change the expectation in the test

    10        self.assertEqual(src.calculator.add(0, 3), 3)
    

    the test passes

  • I add another test with a different number for the first input

     7    def test_addition(self):
     8        self.assertEqual(src.calculator.add(0, 1), 1)
     9        self.assertEqual(src.calculator.add(0, 2), 2)
    10        self.assertEqual(src.calculator.add(0, 3), 3)
    11        self.assertEqual(src.calculator.add(1, 3), 3)
    

    the terminal shows AssertionError

    AssertionError: 4 != 3
    
  • I change the expectation to match reality

    11        self.assertEqual(src.calculator.add(1, 3), 4)
    

    the test passes. The add function looks good so far.

  • I add an assertion with bigger numbers

     7    def test_addition(self):
     8        self.assertEqual(src.calculator.add(0, 1), 1)
     9        self.assertEqual(src.calculator.add(0, 2), 2)
    10        self.assertEqual(src.calculator.add(0, 3), 3)
    11        self.assertEqual(src.calculator.add(1, 3), 4)
    12        self.assertEqual(src.calculator.add(123456, 789012), 4)
    

    the terminal shows AssertionError

    AssertionError: 912468 != 4
    
  • I change the expectation to match reality

    12        self.assertEqual(src.calculator.add(123456, 789012), 912468)
    

    the test passes

  • I add another assertion with a negative number

     7    def test_addition(self):
     8        self.assertEqual(src.calculator.add(0, 1), 1)
     9        self.assertEqual(src.calculator.add(0, 2), 2)
    10        self.assertEqual(src.calculator.add(0, 3), 3)
    11        self.assertEqual(src.calculator.add(1, 3), 4)
    12        self.assertEqual(src.calculator.add(123456, 789012), 912468)
    13        self.assertEqual(src.calculator.add(-1, 0), 912468)
    

    the terminal shows AssertionError

    AssertionError: -1 != 912468
    
  • I change the expectation to match reality

    13        self.assertEqual(src.calculator.add(-1, 0), -1)
    

    the test passes

  • I try another assertion with two negative numbers

    11        self.assertEqual(src.calculator.add(1, 3), 4)
    12        self.assertEqual(src.calculator.add(123456, 789012), 912468)
    13        self.assertEqual(src.calculator.add(-1, 0), -1)
    14        self.assertEqual(src.calculator.add(-2, -3), -1)
    

    the terminal shows AssertionError

    AssertionError: -5 != -1
    
  • I make the expectation match reality

    14        self.assertEqual(src.calculator.add(-2, -3), -5)
    

    the test passes. The add function can handle positive and negative whole numbers

  • I add an assertion with floats (binary floating point decimal numbers)

    12        self.assertEqual(src.calculator.add(123456, 789012), 912468)
    13        self.assertEqual(src.calculator.add(-1, 0), -1)
    14        self.assertEqual(src.calculator.add(-2, -3), -5)
    15        self.assertEqual(src.calculator.add(0.1, 1), -5)
    

    the terminal shows AssertionError

    AssertionError: 1.1 != -5
    
  • I change the expectation

    15        self.assertEqual(src.calculator.add(0.1, 1), 1.1)
    

    the test passes

  • I add another assertion

    13        self.assertEqual(src.calculator.add(-1, 0), -1)
    14        self.assertEqual(src.calculator.add(-2, -3), -5)
    15        self.assertEqual(src.calculator.add(0.1, 1), 1.1)
    16        self.assertEqual(src.calculator.add(0.1, 0.2), 1.1)
    

    the terminal shows AssertionError

    AssertionError: 0.30000000000000004 != 1.1
    

    whoa!

  • I change the expectation

    16        self.assertEqual(
    17            src.calculator.add(0.1, 0.2),
    18            0.30000000000000004
    19        )
    

    the test passes. Why is the result “0.30000000000000004” and not “0.3”?

  • I add another assertion

    14        self.assertEqual(src.calculator.add(-2, -3), -5)
    15        self.assertEqual(src.calculator.add(0.1, 1), 1.1)
    16        self.assertEqual(
    17            src.calculator.add(0.1, 0.2),
    18            0.30000000000000004
    19        )
    20        self.assertEqual(
    21            src.calculator.add(0.1234, -5.6789),
    22            0.30000000000000004
    23        )
    

    the terminal shows AssertionError

    AssertionError: -5.555499999999999 != -5
    

    whaaaaat?!

  • I change the expectation

     7    def test_addition(self):
     8        self.assertEqual(src.calculator.add(0, 1), 1)
     9        self.assertEqual(src.calculator.add(0, 2), 2)
    10        self.assertEqual(src.calculator.add(0, 3), 3)
    11        self.assertEqual(src.calculator.add(1, 3), 4)
    12        self.assertEqual(src.calculator.add(123456, 789012), 912468)
    13        self.assertEqual(src.calculator.add(-1, 0), -1)
    14        self.assertEqual(src.calculator.add(-2, -3), -5)
    15        self.assertEqual(src.calculator.add(0.1, 1), 1.1)
    16        self.assertEqual(
    17            src.calculator.add(0.1, 0.2),
    18            0.30000000000000004
    19        )
    20        self.assertEqual(
    21            src.calculator.add(0.1234, -5.6789),
    22            -5.555499999999999
    23        )
    24
    25
    26# TODO
    

    the test passes. Why is the result “-5.555499999999999” not “-5.5555”?


what is a variable?

I just did the same kind of calculation 10 times, there is a better way to do this thanks to Substitution. I can use a letter or a name for the numbers, so that I can have one test for all possible numbers, it is called a variable.

A variable is a name that is used for values that change. For example, in the tests so far, I have

src.calculator.add(0, 1) is 0 + 1 is 1
src.calculator.add(0, 2) is 0 + 2 is 2
src.calculator.add(0, 3) is 0 + 3 is 3
src.calculator.add(1, 3) is 1 + 3 is 4
src.calculator.add(123456, 789012)  is 123456 +789012 is 912468
src.calculator.add(-2, -3) is -2 + -3 is -5
src.calculator.add(-1, 0) is -1 + 0 is -1
src.calculator.add(0.1, 1) is 0.1 + 1 is 1.1
src.calculator.add(0.1, 0.2) is 0.1 + 0.2 is 0.30000000000000004
src.calculator.add(0.1234, -5.6789) is 0.1234 + -5.6789 is -5.555499999999999

all of these lines can be written using x as the name of the first number and y as the name for the second number, like this

src.calculator.add(x, y) is x + y is x + y
  • I add names to the first assertion

     7    def test_addition(self):
     8        first_number = 0
     9        second_number = 1
    10        self.assertEqual(
    11            src.calculator.add(first_number, second_number),
    12            first_number+second_number
    13        )
    14        self.assertEqual(src.calculator.add(0, 2), 2)
    

    the test is still green

  • I do the same thing to the next assertion

     7    def test_addition(self):
     8        first_number = 0
     9        second_number = 1
    10        self.assertEqual(
    11            src.calculator.add(first_number, second_number),
    12            first_number+second_number
    13        )
    14
    15        first_number = 0
    16        second_number = 2
    17        self.assertEqual(
    18            src.calculator.add(first_number, second_number),
    19            first_number+second_number
    20        )
    

    still green

  • I do the next one

    15        first_number = 0
    16        second_number = 2
    17        self.assertEqual(
    18            src.calculator.add(first_number, second_number),
    19            first_number+second_number
    20        )
    21
    22        first_number = 0
    23        second_number = 3
    24        self.assertEqual(
    25            src.calculator.add(first_number, second_number),
    26            first_number+second_number
    27        )
    

    green

  • then the next one

    22        first_number = 0
    23        second_number = 3
    24        self.assertEqual(
    25            src.calculator.add(first_number, second_number),
    26            first_number+second_number
    27        )
    28
    29        first_number = 1
    30        second_number = 3
    31        self.assertEqual(
    32            src.calculator.add(first_number, second_number),
    33            first_number+second_number
    34        )
    

    still green

  • then the next

    29        first_number = 1
    30        second_number = 3
    31        self.assertEqual(
    32            src.calculator.add(first_number, second_number),
    33            first_number+second_number
    34        )
    35
    36        first_number = 123456
    37        second_number = 789012
    38        self.assertEqual(
    39            src.calculator.add(first_number, second_number),
    40            first_number+second_number
    41        )
    

    the test is still green

  • on to the next assertion

    36        first_number = 123456
    37        second_number = 789012
    38        self.assertEqual(
    39            src.calculator.add(first_number, second_number),
    40            first_number+second_number
    41        )
    42
    43        first_number = -1
    44        second_number = 0
    45        self.assertEqual(
    46            src.calculator.add(first_number, second_number),
    47            first_number+second_number
    48        )
    

    still green

  • and the next

    43        first_number = -1
    44        second_number = 0
    45        self.assertEqual(
    46            src.calculator.add(first_number, second_number),
    47            first_number+second_number
    48        )
    49
    50        first_number = -2
    51        second_number = -3
    52        self.assertEqual(
    53            src.calculator.add(first_number, second_number),
    54            first_number+second_number
    55        )
    

    green

  • more?

    50        first_number = -2
    51        second_number = -3
    52        self.assertEqual(
    53            src.calculator.add(first_number, second_number),
    54            first_number+second_number
    55        )
    56
    57        first_number = 0.1
    58        second_number = 1
    59        self.assertEqual(
    60            src.calculator.add(first_number, second_number),
    61            first_number+second_number
    62        )
    

    still green

  • I add variables to the next assertion

    57        first_number = 0.1
    58        second_number = 1
    59        self.assertEqual(
    60            src.calculator.add(first_number, second_number),
    61            first_number+second_number
    62        )
    63
    64        first_number = 0.1
    65        second_number = 0.2
    66        self.assertEqual(
    67            src.calculator.add(first_number, second_number),
    68            first_number+second_number
    69        )
    

    the test is still green

  • ah, the last one

    64        first_number = 0.1
    65        second_number = 0.2
    66        self.assertEqual(
    67            src.calculator.add(first_number, second_number),
    68            first_number+second_number
    69        )
    70
    71        first_number = 0.1234
    72        second_number = -5.6789
    73        self.assertEqual(
    74            src.calculator.add(first_number, second_number),
    75            first_number+second_number
    76        )
    77
    78
    79# TODO
    

    still green


how to use random numbers

All the assertions in test_addition do the same thing

  • point first_number to a value

  • point second_number to a value

  • call src.calculator.add with first_number and second_number as input

  • check if the result of src.calculator.add(first_number, second_number) is the same as first_number + second_number

I want to use random numbers for first_number and second_number to make sure that the add function always returns the result of adding the two numbers without knowing what the numbers are.

I can do this with the random module from The Python Standard Library, it is a module from The Python Standard Library that is used to make fake random numbers

  • I add an import statement for the random module at the top of test_calculator.py

    1import random
    2import src.calculator
    3import unittest
    
  • I use a random value for first_number in the first assertion

     8    def test_addition(self):
     9        # first_number = 0
    10        first_number = random.triangular(-0.1, 1.0)
    11        second_number = 1
    12        self.assertEqual(
    13            src.calculator.add(first_number, second_number),
    14            first_number+second_number
    15        )
    

    green. random.triangular returns a random float that could be any number from -0.1 to 1.0 in this case, I can also use random.randint if I want a random integer

  • I want to see the test fail to be sure everything works as expected. I change the expectation in the first assertion

     8    def test_addition(self):
     9        # first_number = 0
    10        first_number = random.triangular(-0.1, 1.0)
    11        second_number = 1
    12        self.assertEqual(
    13            src.calculator.add(first_number, second_number),
    14            first_number+first_number
    15        )
    

    I use (ctrl+s (Windows/Linux) or command+s (MacOS)) a few times in the editor to run the tests and the terminal shows AssertionError with random values that look like this

    AssertionError: -X.YZABCDEFGHIJKLM != A.BCDEFGHIJKLMNOPQ
    

    the letters are for random numbers

  • I change the expectation of the assertion back to the right calculation

    12        self.assertEqual(
    13            src.calculator.add(first_number, second_number),
    14            first_number+second_number
    15        )
    

    the test is green again

  • I remove the commented line then use a random value for second_number

     8    def test_addition(self):
     9        first_number = random.triangular(-0.1, 1.0)
    10        # second_number = -5.6789
    11        second_number = random.triangular(-0.1, 1.0)
    

    the test is still green

  • I remove the commented line and the other assertions because they are covered by the one that uses random numbers. I do not need them anymore

     8    def test_addition(self):
     9        first_number = random.triangular(-0.1, 1.0)
    10        second_number = random.triangular(-0.1, 1.0)
    11        self.assertEqual(
    12            src.calculator.add(first_number, second_number),
    13            first_number+second_number
    14        )
    15
    16
    17# TODO
    

    still green

  • I use the Rename Symbol feature to change the name of variables to say what they are

     8def test_addition(self):
     9    random_first_number = random.triangular(-0.1, 1.0)
    10    random_second_number = random.triangular(-0.1, 1.0)
    11
    12    self.assertEqual(
    13        src.calculator.add(
    14            random_first_number,
    15            random_second_number
    16        ),
    17        random_first_number+random_second_number
    18    )
    

    green

  • There is some duplication, If I want to use a different range of random numbers for the test, I have to make a change in more than one place. For example

     8    def test_addition(self):
     9        random_first_number = random.triangular(-10.0, 10.0)
    10        random_second_number = random.triangular(-10.0, 10.0)
    

    still green

  • I add a function to remove the repetition

     1import random
     2import src.calculator
     3import unittest
     4
     5
     6def a_random_number():
     7    return random.triangular(-10.0, 10.0)
     8
     9
    10class TestCalculator(unittest.TestCase):
    
  • I use the new function to get random values for the random_first_number and random_second_number variables

    12    def test_addition(self):
    13        # random_first_number = random.triangular(-10.0, 10.0)
    14        random_first_number = a_random_number()
    15        # random_second_number = random.triangular(-10.0, 10.0)
    16        random_second_number = a_random_number()
    

    the test is still green

  • I remove the commented lines

    12    def test_addition(self):
    13        random_first_number = a_random_number()
    14        random_second_number = a_random_number()
    
  • I change the expectation in the assertion to make sure the test works

    16        self.assertEqual(
    17            src.calculator.add(
    18                random_first_number,
    19                random_second_number
    20            ),
    21            random_second_number+random_second_number
    22        )
    

    the terminal shows AssertionError

    AssertionError: R.STUVWXYZABCDEFG != H.IJKLMNOPQRSTUVWX
    
  • I undo the change

    16    def test_addition(self):
    17        random_first_number = a_random_number()
    18        random_second_number = a_random_number()
    19
    20        self.assertEqual(
    21            src.calculator.add(
    22                random_first_number,
    23                random_second_number
    24            ),
    25            random_first_number+random_second_number
    26        )
    27
    28
    29# TODO
    

    the test is green again

  • I now only need to change the range of random numbers for the test in one place

    6def a_random_number():
    7    return random.triangular(-10000.0, 10000.0)
    

    the test is still green

  • I can use any range of numbers the computer can handle, for example

    6def a_random_number():
    7    return random.triangular(-10.0**100000, 10.0**100000)
    

    the terminal shows OverflowError

    OverflowError: (34, 'Numerical result out of range')
    

    because the numbers are too big

    • ** is the symbol for raise to the power (exponents)

    • 10.0**100000 is how to write 10.0 raised to the power of 100,000

    I make the range smaller

    6def a_random_number():
    7    return random.triangular(-1000.0, 1000.0)
    

    the test is still green, though the test takes a little longer to run

  • I remove test addition from the TODO list

    22# TODO
    23# test subtraction
    24# test multiplication
    25# test division
    26
    27
    28# Exceptions seen
    

I can use a variable to remove duplication


test_subtraction


RED: make it fail


I add a test for subtraction in test_calculator.py

12    def test_addition(self):
13        random_first_number = a_random_number()
14        random_second_number = a_random_number()
15
16        self.assertEqual(
17            src.calculator.add(
18                random_first_number,
19                random_second_number
20            ),
21            random_first_number+random_second_number
22        )
23
24    def test_subtraction(self):
25        random_first_number = a_random_number()
26        random_second_number = a_random_number()
27
28        self.assertEqual(
29            src.calculator.subtract(
30                random_first_number,
31                random_second_number
32            ),
33            random_first_number-random_second_number
34        )
35
36
37# TODO

the terminal shows AttributeError

AttributeError: module 'src.calculator' has no attribute 'subtract'

calculator.py in the src folder does not have anything named subtract in it


GREEN: make it pass


  • I add the name to calculator.py

    1def add(first_input, second_input):
    2    return first_input + second_input
    3
    4
    5subtract
    

    the terminal shows NameError

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

    5subtract = None
    

    the terminal shows TypeError

    TypeError: 'NoneType' object is not callable
    

    I have seen this before

  • I change subtract to a function to make it callable

    5def subtract():
    6    return None
    

    the terminal shows TypeError

    TypeError: subtract() takes 0 positional arguments but 2 were given
    
  • I make subtract take inputs

    5def subtract(first_input, second_input):
    6    return None
    

    the terminal shows AssertionError

    AssertionError: None != XYZ.ABCDEFGHIJKLMNOP
    

    subtract returns None, the test expects

    • random_first_number-random_second_number which is

    • first_input-second_input

    • the difference between the 2 numbers

  • I make the subtract function return the difference between the inputs

    5def subtract(first_input, second_input):
    6    return first_input - second_input
    

    the test passes. SUCCESS!

  • I remove test subtraction from the TODO list in test_calculator.py

    37# TODO
    38# test multiplication
    39# test division
    40
    41
    42# Exceptions seen
    

test_multiplication


RED: make it fail


I add a failing test for multiplication in test_calculator.py

24      def test_subtraction(self):
25          random_first_number = a_random_number()
26          random_second_number = a_random_number()
27
28          self.assertEqual(
29              src.calculator.subtract(
30                  random_first_number,
31                  random_second_number
32              ),
33              random_first_number-random_second_number
34          )
35
36      def test_multiplication(self):
37          random_first_number = a_random_number()
38          random_second_number = a_random_number()
39
40          self.assertEqual(
41              src.calculator.multiply(
42                  random_first_number,
43                  random_second_number
44              ),
45              random_first_number*random_second_number
46          )
47
48
49  # TODO

the terminal shows AttributeError

AttributeError: module 'src.calculator' has no attribute 'multiply'

GREEN: make it pass


  • Using what I know so far, I add a function to calculator.py

     5def subtract(first_input, second_input):
     6    return first_input - second_input
     7
     8
     9def multiply(first_input, second_input):
    10    return first_input * second_input
    

    the test passes

  • I remove test_multiplication from the TODO list in test_calculator.py

    49# TODO
    50# test division
    51
    52
    53# Exceptions seen
    

Note

  • * is the symbol for multiplication

  • ** is the symbol for raise to the power (exponent)


test_division


RED: make it fail


Time for division. I add a new test to test_calculator.py

36    def test_multiplication(self):
37        random_first_number = a_random_number()
38        random_second_number = a_random_number()
39
40        self.assertEqual(
41            src.calculator.multiply(
42                random_first_number,
43                random_second_number
44            ),
45            random_first_number*random_second_number
46        )
47
48    def test_division(self):
49        random_first_number = a_random_number()
50        random_second_number = a_random_number()
51
52        self.assertEqual(
53            src.calculator.divide(
54                random_first_number,
55                random_second_number
56            ),
57            random_first_number/random_second_number
58        )
59
60
61# TODO
62# test division

the terminal shows AttributeError

AttributeError: module 'src.calculator' has no attribute 'divide'

GREEN: make it pass


  • I add the function to calculator.py

     9def multiply(first_input, second_input):
    10    return first_input * second_input
    11
    12
    13def divide(first_input, second_input):
    14    return first_input / second_input
    

    the test passes

  • I remove the TODO list from test_calculator.py

    48    def test_division(self):
    49        random_first_number = a_random_number()
    50        random_second_number = a_random_number()
    51
    52        self.assertEqual(
    53            src.calculator.divide(
    54                random_first_number,
    55                random_second_number
    56            ),
    57            random_first_number/random_second_number
    58        )
    59
    60
    61# Exceptions seen
    62# AssertionError
    63# NameError
    64# AttributeError
    65# TypeError
    

REFACTOR: make it better

  • I have some repetition to remove, the code below happens in every test, that is 4 times in test_calculator.py

    random_first_number = a_random_number()
    random_second_number = a_random_number()
    
  • I add class attributes (variables) to remove the repetition and use the same numbers for all the tests

    10class TestCalculator(unittest.TestCase):
    11
    12    random_first_number = a_random_number()
    13    random_second_number = a_random_number()
    14
    15    def test_addition(self):
    
  • I use the new class attributes in test_addition

    15    def test_addition(self):
    16        # random_first_number = a_random_number()
    17        random_first_number = self.random_first_number
    18        # random_second_number = a_random_number()
    19        random_second_number = self.random_second_number
    20
    21        self.assertEqual(
    

    the test is still green

  • I use the class attributes for the variables in the call to src.calculator.add in the assertion

    21        self.assertEqual(
    22            src.calculator.add(
    23                # random_first_number,
    24                self.random_first_number,
    25                # random_second_number
    26                self.random_second_number
    27            ),
    28            random_first_number+random_second_number
    29        )
    

    still green

  • I use the class attributes for the variables in the expectation of the assertion

    21        self.assertEqual(
    22            src.calculator.add(
    23                # random_first_number,
    24                self.random_first_number,
    25                # random_second_number
    26                self.random_second_number
    27            ),
    28            # random_first_number+random_second_number
    29            self.random_first_number+self.random_second_number
    30        )
    

    green

  • I remove the commented lines and the random_first_number and random_second_number variables from test_addition

    15    def test_addition(self):
    16        self.assertEqual(
    17            src.calculator.add(
    18                self.random_first_number,
    19                self.random_second_number
    20            ),
    21            self.random_first_number+self.random_second_number
    22        )
    23
    24    def test_subtraction(self):
    

    still green


  • I use the new class attributes in test_subtraction

    24    def test_subtraction(self):
    25        # random_first_number = a_random_number()
    26        random_first_number = self.random_first_number
    27        # random_second_number = a_random_number()
    28        random_second_number = self.random_second_number
    29
    30        self.assertEqual(
    

    the test is still green

  • I use the class attributes for the variables in the call to src.calculator.subtract in the assertion

    30        self.assertEqual(
    31            src.calculator.subtract(
    32                # random_first_number,
    33                self.random_first_number,
    34                # random_second_number
    35                self.random_second_number
    36            ),
    37            random_first_number-random_second_number
    38        )
    

    still green

  • I use the class attributes for the variables in the expectation of the assertion

    30        self.assertEqual(
    31            src.calculator.subtract(
    32                # random_first_number,
    33                self.random_first_number,
    34                # random_second_number
    35                self.random_second_number
    36            ),
    37            # random_first_number-random_second_number
    38            self.random_first_number-self.random_second_number
    39        )
    

    green

  • I remove the commented lines and the random_first_number and random_second_number variables from test_subtraction

    24    def test_subtraction(self):
    25        self.assertEqual(
    26            src.calculator.subtract(
    27                self.random_first_number,
    28                self.random_second_number
    29            ),
    30            self.random_first_number-self.random_second_number
    31        )
    32
    33    def test_multiplication(self):
    

    still green


  • I use the new class attributes in test_multiplication

    33    def test_multiplication(self):
    34        # random_first_number = a_random_number()
    35        random_first_number = self.random_first_number
    36        # random_second_number = a_random_number()
    37        random_second_number = self.random_second_number
    38
    39        self.assertEqual(
    

    the test is still green

  • I use the class attributes for the variables in the call to src.calculator.multiply in the assertion

    39        self.assertEqual(
    40            src.calculator.multiply(
    41                # random_first_number,
    42                self.random_first_number,
    43                # random_second_number
    44                self.random_second_number
    45            ),
    46            random_first_number*random_second_number
    47        )
    

    still green

  • I use the class attributes for the variables in the expectation of the assertion

    39        self.assertEqual(
    40            src.calculator.multiply(
    41                # random_first_number,
    42                self.random_first_number,
    43                # random_second_number
    44                self.random_second_number
    45            ),
    46            # random_first_number*random_second_number
    47            self.random_first_number*self.random_second_number
    48        )
    

    green

  • I remove the commented lines and the random_first_number and random_second_number variables from test_multiplication

    33    def test_multiplication(self):
    34        self.assertEqual(
    35            src.calculator.multiply(
    36                self.random_first_number,
    37                self.random_second_number
    38            ),
    39            self.random_first_number*self.random_second_number
    40        )
    41
    42    def test_division(self):
    

    still green


  • I use the new class attributes in test_division

    42    def test_division(self):
    43        # random_first_number = a_random_number()
    44        random_first_number = self.random_first_number
    45        # random_second_number = a_random_number()
    46        random_second_number = self.random_second_number
    47
    48        self.assertEqual(
    

    the test is still green

  • I use the class attributes for the variables in the call to src.calculator.divide in the assertion

    48        self.assertEqual(
    49            src.calculator.divide(
    50                # random_first_number,
    51                self.random_first_number,
    52                # random_second_number
    53                self.random_second_number
    54            ),
    55            random_first_number/random_second_number
    56        )
    

    still green

  • I use the class attributes for the variables in the expectation of the assertion

    48        self.assertEqual(
    49            src.calculator.divide(
    50                # random_first_number,
    51                self.random_first_number,
    52                # random_second_number
    53                self.random_second_number
    54            ),
    55            # random_first_number/random_second_number
    56            self.random_first_number/self.random_second_number
    57        )
    

    green

  • I remove the commented lines and the random_first_number and random_second_number variables from test_division

    42    def test_division(self):
    43        self.assertEqual(
    44            src.calculator.divide(
    45                self.random_first_number,
    46                self.random_second_number
    47            ),
    48            self.random_first_number/self.random_second_number
    49        )
    50
    51
    52# Exceptions seen
    53# AssertionError
    54# NameError
    55# AttributeError
    56# TypeError
    

    still green


All the tests are passing, though they all look the same, there has to be a better way.

The random_first_number and random_second_number variables are made once as class attributes and used later in each test with self.random_first_number and self.random_second_number, the same way I use unittest.TestCase assert methods like assertEqual with self.assertEqual


test_calculator_tests

Since everything is green, I can write the program that makes the tests pass without looking at the tests


RED: make it fail


  • I close test_calculator.py

  • I delete all the text in calculator.py, the terminal shows 4 failures

    FAILED ... - AttributeError: module 'src.calculator' has no attribute 'add'
    FAILED ... - AttributeError: module 'src.calculator' has no attribute 'divide'
    FAILED ... - AttributeError: module 'src.calculator' has no attribute 'multiply'
    FAILED ... - AttributeError: module 'src.calculator' has no attribute 'subtract'
    =========================== 4 failed in X.YZs ============================
    
  • I start with the last AttributeError

    AttributeError: module 'src.calculator' has no attribute 'subtract'
    

What other Exceptions do you think are raised as I go along?


GREEN: make it pass


  • I add the name to calculator.py

    1subtract
    

    the terminal shows NameError

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

    1subtract = None
    

    the terminal shows TypeError

    TypeError: 'NoneType' object is not callable
    
  • I change subtract to a function

    1def subtract():
    2    return None
    

    the terminal shows TypeError

    TypeError: subtract() takes 0 positional arguments but 2 were given
    
  • I add positional arguments to the function

    1def subtract(first_input, second_input):
    2    return None
    

    the terminal shows AssertionError with random numbers

    AssertionError: None != XYZ.ABCDEFGHIJKLMN
    
  • I change the return statement to see the difference between the inputs and expected output, remember the identity function?

    1def subtract(first_input, second_input):
    2    return first_input, second_input
    

    the terminal shows AssertionError with random numbers that look like this

    AssertionError: (XYZ.ABCDEFGHIJKLMN, YZA.BCDEFGHIJKLMNO) != ZAB.CDEFGHIJKLMNOP
    

    the name of the function is subtract and the test expects the difference between the 2 inputs

  • I make the return statement match the expectation

    1def subtract(first_input, second_input):
    2    return first_input - second_input
    

    the terminal shows AttributeError

    AttributeError: module 'src.calculator' has no attribute 'multiply'
    

  • I add a function

    1def subtract(first_input, second_input):
    2    return first_input - second_input
    3
    4
    5def multiply():
    6    return None
    

    the terminal shows TypeError

    TypeError: multiply() takes 0 positional arguments but 2 were given
    

    I add 2 names for the positional arguments

    5def multiply(first_input, second_input):
    6    return None
    

    the terminal shows AssertionError

    AssertionError: None != XY.ZABCDEFGHIJKLM
    
  • I change the return statement to see the difference between the inputs and the expected output, this is the identity function again

    5def multiply(first_input, second_input):
    6    return first_input, second_input
    

    the terminal shows AssertionError with random numbers that look like this

    AssertionError: (XYZ.ABCDEFGHIJKLMNO, -YZA.BCDEFGHIJKLMNOPQ) != -ZAB.CDEFGHIJKLMNOPQR
    
  • I change it to the multiplication of the inputs to match the name of the function

    5def multiply(first_input, second_input):
    6    return first_input * second_input
    

    the terminal shows AttributeError

    AttributeError: module 'src.calculator' has no attribute 'divide'
    

  • I add another function

     5def multiply(first_input, second_input):
     6    return first_input * second_input
     7
     8
     9def divide(first_input, second_input):
    10    return first_input, second_input
    

    the terminal shows AssertionError with random numbers that look like this

    AssertionError: (-XYZ.ABCDEFGHIJKLMNO, YZA.BCDEFGHIJKLMNOPQ) != -ZAB.CDEFGHIJKLMNOPQR
    
  • I change the return statement to give the test what it wants

     9def divide(first_input, second_input):
    10    return first_input / second_input
    

    the terminal shows AttributeError

    AttributeError: module 'src.calculator' has no attribute 'add'
    

  • the return statement of the last 3 functions matched their names, I do the same thing for the new one

     1def subtract(first_input, second_input):
     2    return first_input - second_input
     3
     4
     5def multiply(first_input, second_input):
     6    return first_input * second_input
     7
     8
     9def divide(first_input, second_input):
    10    return first_input / second_input
    11
    12
    13def add(first_input, second_input):
    14    return first_input + second_input
    

    and all the tests are passing with no random failures, or are they?


close the project

  • I close calculator.py in the editor

  • I click in the terminal, then use q on the keyboard to leave the tests. The terminal goes back to the command line

  • I change directory to the parent of calculator

    cd ..
    

    the terminal shows

    .../pumping_python
    

    I am back in the pumping_python directory


review

I wrote these tests for a program that can add, subtract, multiply and divide

I also saw these Exceptions


code from the chapter

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


what is next?

you know a lot, you know

Would you like to see a better way to write test_why_use_a_function?