how to make a calculator


In this chapter I write a program that does the arithmetic operations of addition, subtraction, multiplication and division


test_addition

red: make it fail

  • 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 in the editor

  • then change True to False to make the test pass

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

        def test_failure(self):
            self.assertFalse(False)
    
    
    # TODO
    # test addition
    # test subtraction
    # test multiplication
    # test division
    
    
    # Exceptions Encountered
    # AssertionError
    
  • and change test_failure to test_addition then add an assertion

    import unittest
    
    
    class TestCalculator(unittest.TestCase):
    
        def test_addition(self):
            self.assertEqual(
                src.calculator.add(0, 1),
                1
            )
    
    • 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

    but 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

    # Exceptions Encountered
    # AssertionError
    # NameError
    
  • then add an import statement

    import src.calculator
    import unittest
    
    
    class TestCalculator(unittest.TestCase):
    ...
    

    and 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

    # Exceptions Encountered
    # AssertionError
    # NameError
    # AttributeError
    
  • then open calculator.py in the editor to put the name

    add
    

    the terminal shows NameError

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

    add = None
    

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

    # Exceptions Encountered
    # AssertionError
    # NameError
    # AttributeError
    # TypeError
    
  • then change add to a function with the def keyword to make it callable

    def add():
        return None
    

    the terminal shows another TypeError

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

    add currently takes in 0 inputs, but 2 were provided in the test - 0 and 1

  • I make it take 2 positional arguments

    def add(x, y):
        return None
    

    and get AssertionError

    AssertionError: None != 1
    

    the add function returns None and the test expects 1

  • when I make the return statement match the expected value

    def add(x, y):
        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, it does not care about the inputs. I want it to do a calculation with the inputs and return the result

red: make it fail

I add an assertion to show the problem with the function

def test_addition(self):
    self.assertEqual(
        src.calculator.add(0, 1),
        1
    )
    self.assertEqual(
        src.calculator.add(-1, 1),
        0
    )

and get AssertionError

E    AssertionError: 1 != 0

the function returns 1 and the test expects 0

green: make it pass

when I change the return statement to the sum of the inputs

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

the terminal shows a passing test

refactor: make it better

  • I add an import statement to use random numbers in the test

    import random
    import src.calculator
    import 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

    class TestCalculator(unittest.TestCase):
    
        def test_addition(self):
            x = random.randint(-1, 1)
            y = random.randint(-1, 1)
    
            self.assertEqual(
                src.calculator.add(x, y),
                x+x
            )
            self.assertEqual(
                src.calculator.add(0, 1),
                1
            )
            self.assertEqual(
                src.calculator.add(-1, 1),
                0
            )
    

    because the range of numbers is small, the terminal shows random success or AssertionError

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

    I change the expectation in the assertion to the correct calculation

    self.assertEqual(
        src.calculator.add(x, y),
        x+y
    )
    

    and the terminal shows passing tests

    • x = random.randint(-1, 1) points a variable called x to the result of calling random.randint(-1, 1) which gives me a random number from -1 up to and including 1

    • -1 for negative numbers, 0 for itself, and 1 for positive numbers

  • I remove the other assertions because they are covered by the one that uses random numbers

    class TestCalculator(unittest.TestCase):
    
        def test_addition(self):
            x = random.randint(-1, 1)
            y = random.randint(-1, 1)
    
            self.assertEqual(
                src.calculator.add(x, y),
                x+y
            )
    
  • When I want to use a different range of random numbers for the test, I have to make a change in more than one place

    def test_addition(self):
        x = random.randint(-10, 10)
        y = random.randint(-10, 10)
    
        self.assertEqual(
            src.calculator.add(x, y),
            x+y
        )
    

    I add a function to remove the duplication of calls to random.randint

    import random
    import src.calculator
    import unittest
    
    
    def a_random_number():
        return random.randint(-1, 1)
    

    then call it for the x and y variables

    def test_addition(self):
        x = a_random_number()
        y = a_random_number()
    
        self.assertEqual(
            src.calculator.add(x, y),
            x+y
        )
    

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

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

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

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

    the test is still green and takes longer to run. I change the range back to -10, 10

  • then remove test addition from the TODO list

    # TODO
    # test subtraction
    # test multiplication
    # test division
    

test_subtraction

red: make it fail

  • I add a method to test subtraction

    def test_addition(self):
        x = a_random_number()
        y = a_random_number()
    
        self.assertEqual(
            src.calculator.add(x, y),
            x+y
        )
    
    def test_subtraction(self):
        x = a_random_number()
        y = a_random_number()
    
        self.assertEqual(
            src.calculator.subtract(x, y),
            x-y
        )
    

    which gives me AttributeError

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

green: make it pass

  • I add the name to calculator.py

    def add(x, y):
        return x + y
    
    
    subtract
    

    the terminal shows NameError

    NameError: name 'subtract' is not defined
    

    then I point it to None

    subtract = None
    

    and get TypeError

    TypeError: 'NoneType' object is not callable
    

    I have seen this before

  • I make it a function to make it callable

    def subtract():
        return None
    

    and the terminal shows another TypeError

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

    def subtract(x, y):
        return None
    

    and get AssertionError

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

    subtract returns None and the test expects x-y

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

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

    the test passes. SUCCESS!

refactor: make it better

  • I have some duplication to remove

    x = a_random_number()
    y = a_random_number()
    

    the code above happen twice, once in test_addition and again in test_subtraction. I can use class attributes (variables) to remove them and use the same numbers for both tests

    class TestCalculator(unittest.TestCase):
    
        x = a_random_number()
        y = a_random_number()
    
        def test_addition(self):
            x = self.x
            y = self.y
    
            self.assertEqual(
                src.calculator.add(x, y),
                x+y
            )
    
        def test_subtraction(self):
            x = self.x
            y = self.y
    
            self.assertEqual(
                src.calculator.subtract(x, y),
                x-y
            )
    

    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 remove the x and y variables from test_addition and test_subtraction

    class TestCalculator(unittest.TestCase):
    
        x = a_random_number()
        y = a_random_number()
    
        def test_addition(self):
            self.assertEqual(
                src.calculator.add(self.x, self.y),
                self.x+self.y
            )
    
        def test_subtraction(self):
            self.assertEqual(
                src.calculator.subtract(self.x, self.y),
                self.x-self.y
            )
    

    and the tests are still green!

  • I remove test subtraction from the TODO list

    # TODO
    # test multiplication
    # test division
    

test_multiplication

red: make it fail

I add a failing test for multiplication

def test_subtraction(self):
    self.assertEqual(
        src.calculator.subtract(self.x, self.y),
        self.x-self.y
    )

def test_multiplication(self):
    self.assertEqual(
        src.calculator.multiply(self.x, self.y),
        self.x*self.y
    )

and 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

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


def multiply(x, y):
    return x * y

which gives me passing tests. I remove test_multiplication from the TODO list

# TODO
# test division

test_division

red: make it fail

time for division

def test_multiplication(self):
    self.assertEqual(
        src.calculator.multiply(self.x, self.y),
        self.x*self.y
    )

def test_division(self):
    self.assertEqual(
        src.calculator.divide(self.x, self.y),
        self.x/self.y
    )

the terminal shows AttributeError

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

green: make it pass

  • I add a function to calculator.py

    def divide(x, y):
        return x / y
    

    then make the range of numbers for the tests smaller

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

    and when y is randomly 0 the terminal shows a ZeroDivisionError

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

    dividing by 0 is not defined in mathematics and raises an Exception in Python

  • I add it to the list of Exceptions encountered

    # Exceptions Encountered
    # AssertionError
    # NameError
    # AttributeError
    # TypeError
    # ZeroDivisionError
    

how to test that an Exception is raised

red: make it fail

I add a line to raise the ZeroDivisionError and comment out the code that randomly fails

def test_division(self):
    src.calculator.divide(self.x, 0)

    # self.assertEqual(
    #    src.calculator.divide(self.x, self.y),
    #    self.x/self.y
    # )

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

x = 0, y = 0

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

Exceptions like ZeroDivisionError break execution of a program. No code will run past the line that caused one, which means I have to take care of this one. See how to test that an Exception is raised for more

green: make it pass

  • I can use the unittest.TestCase.assertRaises method to make sure that a ZeroDivisionError is raised when I try to divide a number by 0

    def test_division(self):
        with self.assertRaises(AssertionError):
            src.calculator.divide(self.x, 0)
    
        # self.assertEqual(
        #   src.calculator.divide(self.x, self.y),
        #   self.x/self.y
        # )
    

    because I used the wrong Exception the terminal still shows a ZeroDivisionError

    ZeroDivisionError: division by zero
    
  • When I change it to use the right one

    with self.assertRaises(ZeroDivisionError):
        src.calculator.divide(self.x, 0)
    

    the test passes, showing that the code raises the Exception

refactor: make it better

  • I still have a problem since self.y can sometimes be 0, I use a while statement to make sure it never happens in the assertion

    def test_division(self):
        with self.assertRaises(ZeroDivisionError):
            src.calculator.divide(self.x, 0)
    
        while self.y == 0:
            self.y = a_random_number()
        else:
            self.assertEqual(
                src.calculator.divide(self.x, self.y),
                self.x/self.y
            )
    

    here is what it does

    • if the value of self.y is 0

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

      • then checks if the value of self.y is 0 again, repeating the process until self.y is not 0

    • if the value of self.y is not 0 at any point, it exits the 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

    def test_division(self):
        with self.assertRaises(ZeroDivisionError):
            src.calculator.divide(self.x, 0)
    
        while self.y == 0:
            src.calculator.divide(self.x, self.y)
            self.y = a_random_number()
        else:
            self.assertEqual(
                src.calculator.divide(self.x, self.y),
                self.x/self.y
            )
    

    when self.y is randomly 0, the terminal shows a ZeroDivisionError

    ZeroDivisionError: division by zero
    
  • I add an assertRaises block to catch the Exception in the while statement and remove the previous statement from the test because it is now part of the loop

    def test_division(self):
        while self.y == 0:
            with self.assertRaises(ZeroDivisionError):
                src.calculator.divide(self.x, self.y)
            self.y = a_random_number()
        else:
            self.assertEqual(
                src.calculator.divide(self.x, self.y),
                self.x/self.y
            )
    

    the terminal shows all tests are passing with no random failures

  • I use a bigger range of numbers for the tests

    def a_random_number():
        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

  • then remove the TODO list


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

    subtract
    

    and get NameError

    NameError: name 'subtract' is not defined
    

    I point it to None

    subtract = None
    

    which gives me TypeError

    TypeError: 'NoneType' object is not callable
    

    I make it a function

    def subtract():
        return None
    

    and the terminal shows another TypeError

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

    I add positional arguments to the function

    def subtract(a, b):
        return None
    

    and get AssertionError

    AssertionError: None != -10
    AssertionError: None != -9
    AssertionError: None != 0
    AssertionError: None != 12
    
  • then change the return statement to see the difference between the inputs and expected output

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

    and the terminal shows another 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

    def subtract(a, b):
        return a - b
    

    and the terminal shows another AttributeError

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

    def multiply():
        return None
    

    and get TypeError

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

    then add 2 variables for the positional arguments

    def multiply(a, b):
        return None
    

    which gives me AssertionError

    AssertionError: None != -42
    AssertionError: None != -10
    AssertionError: None != 20
    AssertionError: None != 36
    
  • I change the return statement to see the difference between the inputs and the expected output

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

    and the terminal shows AssertionError

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

    I change it to the product of the inputs, matching the name of the function

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

    and get another AttributeError

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

    def divide(a, b):
        return a, b
    

    and 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

    def divide(a, b):
        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 use that for the new one

    def add(a, b):
        return a + b
    

    and all tests are passing with no random failures. Lovely!


review

I wrote the following tests for a program that performs the arithmetic operations

I also ran into the following Exceptions

Would you like to test passing values?


how to make a calculator: tests and solutions