how to make a calculator


I want to write a program that can add, subtract, multiply and divide

requirements

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

    ./makePythonTdd.sh calculator
    

    on Windows without Windows Subsystem Linux use makePythonTdd.ps1

    ./makePythonTdd.ps1 calculator
    

    the terminal shows AssertionError

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

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

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

    4class TestCalculator(unittest.TestCase):
    
  • I add a TODO list to keep track of the work for the program

    Note

    the line numbers below are a guide, you do not need to copy them

     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 Encountered
    18# AssertionError
    

test_addition

  • 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(
     8            src.calculator.add(0, 1),
     9            1
    10        )
    
    • the assertEqual method from the unittest.TestCase class checks if its 2 inputs are the same. It is like the statement assert x == y or asking is x equal to y?

    • the explanation I like from what I have seen is that one of them is

      • reality - src.calculator.add(0, 1), and the other is my

      • expectation - 1, because 0 plus 1 is 1

    the terminal shows NameError

    NameError: name 'src' is not defined
    

    because src is not defined in test_calculator.py

green: make it pass

  • I add the error to the list of Exceptions encountered in test_calculator.py

    20# Exceptions Encountered
    21# AssertionError
    22# NameError
    
  • then I add an import statement at the top of the file

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

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

  • I add the error to the list of Exceptions encountered in test_calculator.py

    21# Exceptions Encountered
    22# AssertionError
    23# NameError
    24# AttributeError
    
  • then I click on calculator.py in the src folder to open it in the editor, and I type the name

    1add
    

    the terminal shows NameError

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

    1add = None
    

    the terminal shows TypeError

    TypeError: 'NoneType' object is not callable
    

    because the add variable is None which is not callable

  • I add the error to the list of Exceptions encountered in test_calculator.py

    21# Exceptions Encountered
    22# AssertionError
    23# NameError
    24# AttributeError
    25# TypeError
    
  • then I change add to a function to make it callable with the def keyword in calculator.py

    Note

    the line numbers below are a guide, you do not need to copy them

    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 take input, but 2 were given in the call src.calculator.add(0, 1) - 0 and 1

  • I make it take 2 arguments

    1def add(x, y):
    2    return None
    

    the terminal shows AssertionError

    AssertionError: None != 1
    

    the add function returns None, the test expects 1

  • when I make the return statement match the expected value

    1def add(x, y):
    2    return 1
    

    the test passes, time for a victory lap!

refactor: make it better

The add function passes the test but does not meet the actual requirement because it always returns 1. I want it to do a calculation with the inputs and return the result

red: make it fail

To show the problem with the function, I add another assertion in test_calculator.py

 7      def test_addition(self):
 8          self.assertEqual(
 9              src.calculator.add(0, 1),
10              1
11          )
12          self.assertEqual(
13              src.calculator.add(-1, 1),
14              0
15          )

the terminal shows AssertionError

E    AssertionError: 1 != 0

the function returns 1, the test expects 0

green: make it pass

when I change the return statement in calculator.py to add the two inputs

1def add(x, y):
2    return x + y

the test passes

refactor: make it better

  • I want the test to use random numbers instead of numbers that do not change, so I add an import statement at the top of test_calculator.py to use random numbers in the test

    1import random
    2import src.calculator
    3import unittest
    

    random is a module from the python standard library that is used to make fake random numbers

  • then I add variables and a new assertion

     6class TestCalculator(unittest.TestCase):
     7
     8    def test_addition(self):
     9        x = random.randint(-1, 1)
    10        y = random.randint(-1, 1)
    11
    12        self.assertEqual(
    13            src.calculator.add(x, y),
    14            x+x
    15        )
    16        self.assertEqual(
    17            src.calculator.add(0, 1),
    18            1
    19        )
    20        self.assertEqual(
    21            src.calculator.add(-1, 1),
    22            0
    23        )
    

    I hit save (ctrl+s (windows/linux) or command+s (mac)) a few times in the editor to run the tests and the terminal shows random success or AssertionError

    AssertionError: 0 != 2
    AssertionError: -1 != -2
    AssertionError: -1 != 0
    AssertionError: 1 != 2
    

    I change the expectation of the assertion in the test to the correct calculation

    12        self.assertEqual(
    13            src.calculator.add(x, y),
    14            x+y
    15        )
    

    the test passes

    • random.randint(-1, 1) returns a random number from -1 up to and including 1

      • -1 for negative numbers

      • 0 for 0

      • 1 for positive numbers

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

     6class TestCalculator(unittest.TestCase):
     7
     8    def test_addition(self):
     9        x = random.randint(-1, 1)
    10        y = random.randint(-1, 1)
    11
    12        self.assertEqual(
    13            src.calculator.add(x, y),
    14            x+y
    15        )
    16
    17
    18# TODO
    19...
    
  • There is some duplication, I have to make a change in more than one place when I want to use a different range of random numbers for the test

     8    def test_addition(self):
     9        x = random.randint(-10, 10)
    10        y = random.randint(-10, 10)
    11
    12        ...
    

    I add a function to remove the repetition

    Note

    the line numbers below are a guide, you do not need to copy them

     1import random
     2import src.calculator
     3import unittest
     4
     5
     6def a_random_number():
     7    return random.randint(-1, 1)
     8
     9
    10class TestCalculator(unittest.TestCase):
    11
    12    ...
    

    then I use the new function for the x and y variables in test_addition

    12    def test_addition(self):
    13        x = a_random_number()
    14        y = a_random_number()
    15
    16        ...
    

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

    6def a_random_number():
    7    return random.randint(-10, 10)
    

    and the terminal still shows green. I can use any range of numbers the computer can handle, for example

    6def a_random_number():
    7    return random.randint(-10**100000, 10**100000)
    

    the test is still green and takes longer to run. 10**100000 is how to write 10 raised to the power of 100,000. I change the range back to -10, 10 to keep the test running fast

    6def a_random_number():
    7    return random.randint(-10, 10)
    
  • then I remove test addition from the TODO list

    22# TODO
    23# test subtraction
    24# test multiplication
    25# test division
    

test_subtraction

red: make it fail

  • I add a test for subtraction in test_calculator.py

    10class TestCalculator(unittest.TestCase):
    11
    12    def test_addition(self):
    13        x = a_random_number()
    14        y = a_random_number()
    15
    16        self.assertEqual(
    17            src.calculator.add(x, y),
    18            x+y
    19        )
    20
    21    def test_subtraction(self):
    22        x = a_random_number()
    23        y = a_random_number()
    24
    25        self.assertEqual(
    26            src.calculator.subtract(x, y),
    27            x-y
    28        )
    

    the terminal shows AttributeError

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

green: make it pass

  • I add the name to calculator.py

    Note

    the line numbers below are a guide, you do not need to copy them

    1def add(x, y):
    2    return x + y
    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(x, y):
    6    return None
    

    the terminal shows AssertionError

    AssertionError: None != -17
    AssertionError: None != -4
    AssertionError: None != 7
    AssertionError: None != 10
    

    subtract returns None, the test expects x-y

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

    5def subtract(x, y):
    6    return x - y
    

    the test passes. SUCCESS!

refactor: make it better

  • I have some duplication to remove, the code below happens twice

    x = a_random_number()
    y = a_random_number()
    

    once in test_addition and again in test_subtraction. I add class attributes (variables) to remove the duplication and use the same numbers for both tests

    10class TestCalculator(unittest.TestCase):
    11
    12    x = a_random_number()
    13    y = a_random_number()
    

    I use the new class attributes in test_addition

    15    def test_addition(self):
    16        x = self.x
    17        y = self.y
    18
    19        self.assertEqual(
    20            src.calculator.add(x, y),
    21            x+y
    22        )
    

    and in test_subtraction

    24    def test_subtraction(self):
    25        x = self.x
    26        y = self.y
    27
    28        self.assertEqual(
    29            src.calculator.subtract(x, y),
    30            x-y
    31        )
    

    the terminal shows the tests are still passing. The x and y variables are made once as class attributes (variables) and used later in each test with self.x and self.y, the same way I use unittest.TestCase methods like assertEqual or assertFalse

  • I can use the class attributes directly in test_addition

    19        self.assertEqual(
    20            src.calculator.add(self.x, self.y),
    21            self.x+self.y
    22        )
    

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

    28        self.assertEqual(
    29            src.calculator.subtract(self.x, self.y),
    30            self.x-self.y
    31        )
    
  • I remove the x and y variables from test_addition and test_subtraction since they are no longer needed

    10class TestCalculator(unittest.TestCase):
    11
    12    x = a_random_number()
    13    y = a_random_number()
    14
    15    def test_addition(self):
    16        self.assertEqual(
    17            src.calculator.add(self.x, self.y),
    18            self.x+self.y
    19        )
    20
    21    def test_subtraction(self):
    22        self.assertEqual(
    23            src.calculator.subtract(self.x, self.y),
    24            self.x-self.y
    25        )
    26
    27
    28# TODO
    29...
    

    and the tests are still green!

  • I remove test subtraction from the TODO list

    28# TODO
    29# test multiplication
    30# test division
    

test_multiplication

red: make it fail

I add a failing test for multiplication in test_calculator.py

21    def test_subtraction(self):
22        self.assertEqual(
23            src.calculator.subtract(self.x, self.y),
24            self.x-self.y
25        )
26
27    def test_multiplication(self):
28        self.assertEqual(
29            src.calculator.multiply(self.x, self.y),
30            self.x*self.y
31        )
32
33# TODO
34...

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(x, y):
 6    return x - y
 7
 8
 9def multiply(x, y):
10    return x * y

the test passes! I remove test_multiplication from the TODO list in test_calculator.py

34# TODO
35# test division

test_division

red: make it fail

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

27    def test_multiplication(self):
28        self.assertEqual(
29            src.calculator.multiply(self.x, self.y),
30            self.x*self.y
31        )
32
33    def test_division(self):
34        self.assertEqual(
35            src.calculator.divide(self.x, self.y),
36            self.x/self.y
37        )
38
39# TODO
40...

the terminal shows AttributeError

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

green: make it pass

  • I add a function to calculator.py

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

    then I make the range of numbers for the tests smaller in test_calculator.py

    6def a_random_number():
    7    return random.randint(-1, 1)
    

    I hit save (ctrl+s (windows/linux) or command+s (mac)) a few times to run the tests, and when y is randomly 0 the terminal shows ZeroDivisionError

    x = -1, y = 0
    x = 0, y = 0
    x = 1, y = 0
    
        def divide(x, y):
    >       return x / y
                   ^^^^^
    E       ZeroDivisionError: division by zero
    

    dividing by 0 is undefined in mathematics and raises ZeroDivisionError in Python

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

    44# Exceptions Encountered
    45# AssertionError
    46# NameError
    47# AttributeError
    48# TypeError
    49# ZeroDivisionError
    

how to test that ZeroDivisionError is raised

red: make it fail

I add a line to cause ZeroDivisionError intentionally and comment out the code that randomly fails in test_calculator.py

33    def test_division(self):
34        src.calculator.divide(self.x, 0)
35
36        # self.assertEqual(
37        #    src.calculator.divide(self.x, self.y),
38        #    self.x/self.y
39        # )

the terminal shows my expectation with a failure for any value of x since y is 0

x = -1, y = 0
x = 0, y = 0
x = 1, y = 0

    def divide(x, y):
>       return x / y
                ^^^^^
E       ZeroDivisionError: division by zero

Exceptions(Errors) like ZeroDivisionError stop a program from running. No code will run past the line that causes an Exception(Error), which means I have to take care of this problem. See how to test that an Exception is raised for more

green: make it pass
  • I can use the assertRaises method to make sure that ZeroDivisionError is raised when I try to divide a number by 0

    33    def test_division(self):
    34        with self.assertRaises(AssertionError):
    35            src.calculator.divide(self.x, 0)
    36
    37        # self.assertEqual(
    38        #   src.calculator.divide(self.x, self.y),
    39        #   self.x/self.y
    40        # )
    

    because I used the wrong Exception the terminal still shows ZeroDivisionError

    ZeroDivisionError: division by zero
    
  • I change it to the right Exception

    33    def test_division(self):
    34        with self.assertRaises(ZeroDivisionError):
    35            src.calculator.divide(self.x, 0)
    

    the test passes, showing that src.calculator.divide(self.x, 0) raises ZeroDivisionError

refactor: make it better
  • I still have a problem because self.y can sometimes be 0, I use a while statement to make a never ending loop to make sure it never happens in the assertion in test_calculator.py

    33    def test_division(self):
    34        with self.assertRaises(ZeroDivisionError):
    35            src.calculator.divide(self.x, 0)
    36
    37        while self.y == 0:
    38            self.y = a_random_number()
    39        else:
    40            self.assertEqual(
    41                src.calculator.divide(self.x, self.y),
    42                self.x/self.y
    43            )
    

    here is what it does

    • when the value of self.y is 0

      • it points self.y to the result of calling a_random_number()

      • then it checks if the value of self.y is 0 again. The process happens again non stop until self.y is not 0

    • when the value of self.y is not 0, it leaves the while loop and runs the code in the else block

  • Since self.y is 0 in the first part of the while statement I can add a call to the divide function that will fail

    33    def test_division(self):
    34        with self.assertRaises(ZeroDivisionError):
    35            src.calculator.divide(self.x, 0)
    36
    37        while self.y == 0:
    38            src.calculator.divide(self.x, self.y)
    39            self.y = a_random_number()
    40        else:
    41            self.assertEqual(
    42                src.calculator.divide(self.x, self.y),
    43                self.x/self.y
    44            )
    

    I hit save (ctrl+s (windows/linux) or command+s (mac)) in the editor a few times to run the tests, and when self.y is randomly 0, the terminal shows ZeroDivisionError

    ZeroDivisionError: division by zero
    
  • I add assertRaises to catch the Exception in the while statement

    33    def test_division(self):
    34        with self.assertRaises(ZeroDivisionError):
    35            src.calculator.divide(self.x, 0)
    36
    37        while self.y == 0:
    38            with self.assertRaises(ZeroDivisionError):
    39                  src.calculator.divide(self.x, self.y)
    40            self.y = a_random_number()
    41        else:
    42            self.assertEqual(
    43                src.calculator.divide(self.x, self.y),
    44                self.x/self.y
    45            )
    
  • I no longer need the first assertRaises and remove it from the test because it is now part of the while loop

    33    def test_division(self):
    34        while self.y == 0:
    35            with self.assertRaises(ZeroDivisionError):
    36                src.calculator.divide(self.x, self.y)
    37            self.y = a_random_number()
    38        else:
    39            self.assertEqual(
    40                src.calculator.divide(self.x, self.y),
    41                self.x/self.y
    42            )
    43
    44
    45# TODO
    46...
    

    the terminal shows all tests are passing with no random failures

  • I use a bigger range of numbers for the tests

    6def a_random_number():
    7    return random.randint(-10**1000000, 10**1000000)
    

    the terminal still shows green and it takes longer to run the tests. I change the range back to -10, 10 to keep the tests fast

    6def a_random_number():
    7    return random.randint(-10, 10)
    
  • then I remove the TODO list

    45# Exceptions Encountered
    46...
    

test_calculator_tests

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

red: make it fail

  • I close test_calculator.py

  • then delete all the text in calculator.py, the terminal shows AttributeError

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

    can you tell what Exceptions will show up 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

    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(a, b):
    2    return None
    

    the terminal shows AssertionError

    AssertionError: None != X
    
  • I change the return statement to see the difference between the inputs and expected output

    1def subtract(a, b):
    2    return a, b
    

    the terminal shows random numbers with AssertionError

    AssertionError: (-10, 2) != -12
    AssertionError: (-1, 7) != -8
    AssertionError: (10, 6) != 4
    AssertionError: (7, -10) != 17
    

    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(a, b):
    2    return a - b
    

    the terminal shows AttributeError

    AttributeError: module 'src.calculator' has no attribute 'multiply'
    
  • I add a function

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

    the terminal shows TypeError

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

    I add 2 variables for the positional arguments

    5def multiply(a, b):
    6    return None
    

    the terminal shows AssertionError

    AssertionError: None != X
    
  • I change the return statement to see the difference between the inputs and the expected output

    5def multiply(a, b):
    6    return a, b
    

    the terminal shows random numbers with AssertionError

    AssertionError: (-6, 6) != -36
    AssertionError: (-2, 3) != -6
    AssertionError: (2, 5) != 10
    AssertionError: (-9, -5) != 45
    

    I change it to the multiplication of the inputs to match the name of the function

    5def multiply(a, b):
    6    return a * b
    

    the terminal shows AttributeError

    AttributeError: module 'src.calculator' has no attribute 'divide'
    
  • I add another function

     1def subtract(a, b):
     2    return a - b
     3
     4
     5def multiply(a, b):
     6    return a * b
     7
     8
     9def divide(a, b):
    10    return a, b
    

    the terminal shows AssertionError

    AssertionError: (-10, 6) != -1.6666666666666667
    AssertionError: (-6, -6) != 1.0
    AssertionError: (5, 7) != 0.7142857142857143
    AssertionError: (10, 9) != 1.1111111111111112
    

    or

    AssertionError: ZeroDivisionError not raised
    

    when I change the return statement to match the expectation

     9def divide(a, b):
    10    return a / b
    

    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(a, b):
     2    return a - b
     3
     4
     5def multiply(a, b):
     6    return a * b
     7
     8
     9def divide(a, b):
    10    return a / b
    11
    12
    13def add(a, b):
    14    return a + b
    

    and all the tests are passing with no random failures. Lovely! I am a Programmer!


review

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

I also ran into the following Exceptions

Would you like to test passing values?


Click Here to see the code from this chapter