how to make a calculator


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

preview

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

 1import random
 2import src.calculator
 3import unittest
 4
 5
 6def a_random_number():
 7    return random.triangular(-10, 10)
 8
 9
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):
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):
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):
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):
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

start the project

  • I name this project calculator

  • I open a terminal

  • then I make a directory for the project

    mkdir calculator
    

    the terminal goes back to the command line

    .../pumping_python
    
  • I change directory to the project

    cd calculator
    

    the terminal shows I am now in the calculator folder

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

    mkdir src
    

    the terminal goes back to the command line

    .../pumping_python/calculator
    
  • I use touch to make an empty file for the program in the src folder

    touch src/calculator.py
    

    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

    .../pumping_python/calculator
    
  • I make a directory for the tests

    mkdir tests
    

    the terminal goes back to the command line

  • I use touch to make an empty file in the tests folder to tell Python that it is a Python package

    Attention

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

    touch tests/__init__.py
    

    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 an empty file for the actual test

    touch tests/test_calculator.py
    

    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 Visual Studio Code by typing code and the name of the file with

    code tests/test_calculator.py
    

    test_calculator.py opens up 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 virtual environment in the terminal

    python3 -m venv .venv
    

    on Windows without Windows Subsystem for Linux use python3 -m venv .venv instead of python3 -m venv .venv

    python -m venv .venv
    

    the terminal takes some time then goes back to the command line

  • I activate the virtual environment

    source .venv/bin/activate
    

    on Windows without Windows Subsystem for Linux use .venv/bin/activate.ps1 instead of source .venv/bin/activate

    .venv/scripts/activate.ps1
    

    the terminal shows

    (.venv) .../pumping_python/calculator
    
  • I upgrade the Python package manager (pip) to the latest version

    python3 -m pip install --upgrade pip
    

    the terminal shows pip being uninstalled then installs the latest version or shows that it is already the latest version

  • I make a requirements.txt file for the Python programs my project needs

    echo "pytest-watch" > requirements.txt
    

    the terminal goes back to the command line

  • I use pip to use the requirements file to install pytest-watch

    python3 -m pip install --requirement requirements.txt
    

    on Windows without Windows Subsystem for Linux use python -m pip install --requirement requirements.txt instead of python3 -m pip install --requirement requirements.txt

    python -m pip install --requirement requirements.txt
    

    the terminal shows pip downloads and installs the Python programs that pytest-watch needs to run

  • I use pytest-watch to run the test

    pytest-watch
    

    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 or command (MacOS) on the keyboard and use the mouse to click on tests/test_calculator.py:7 to open it in the editor

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

     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(
     8            src.calculator.add(0, 1),
     9            1
    10        )
    
    • 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?

    • 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 + 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 NameError to the list of Exceptions seen in test_calculator.py

    20# Exceptions seen
    21# AssertionError
    22# 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 (directory)

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

    21# Exceptions seen
    22# AssertionError
    23# NameError
    24# AttributeError
    
  • I open calculator.py from the src folder in the editor, and I type the name

    1add
    

    the terminal shows NameError

    NameError: name 'add' is not defined
    

    I have to tell Python what the name add stands for or means

  • I point it to None

    1add = None
    

    the terminal shows TypeError

    TypeError: 'NoneType' object is not callable
    

    because the add variable is now a name for None which I cannot use like a function

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

    21# Exceptions seen
    22# AssertionError
    23# NameError
    24# AttributeError
    25# 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 src.calculator.add(0, 1): 0 and 1

  • I make the add 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 match the expected value

    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 meet the actual requirement because it always returns 1. I want it to return the result of a calculation with the inputs

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(0, 2),
14              2
15          )

the terminal shows AssertionError

E    AssertionError: 1 != 2

the function returns 1, the test expects 2

GREEN: make it pass

  • when I change the return statement in calculator.py to match the expectation

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

    the terminal shows AssertionError

    AssertionError: 2 != 1
    

    this makes the assertion that was passing before now fail. I need a solution that can make the two tests pass

  • 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 test passes. The add function passes the two tests

RED: make it fail

I add another test to make sure the function works for other numbers

12        self.assertEqual(
13            src.calculator.add(0, 2),
14            2
15        )
16        self.assertEqual(
17            src.calculator.add(0, 3),
18            2
19        )

the terminal shows AssertionError

AssertionError: 3 != 2

GREEN: make it pass

I change the expectation in the test

16        self.assertEqual(
17            src.calculator.add(0, 3),
18            3
19        )

the test passes

RED: make it fail

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

16        self.assertEqual(
17            src.calculator.add(0, 3),
18            3
19        )
20        self.assertEqual(
21            src.calculator.add(1, 3),
22            3
23        )

the terminal shows AssertionError

AssertionError: 4 != 3

GREEN: make it pass

I change the expectation to match reality

20        self.assertEqual(
21            src.calculator.add(1, 3),
22            4
23        )

the test passes. The add function looks good so far

RED: make it fail

I add a test with bigger numbers

20        self.assertEqual(
21            src.calculator.add(1, 3),
22            4
23        )
24        self.assertEqual(
25            src.calculator.add(123456, 789012),
26            4
27        )

the terminal shows AssertionError

AssertionError: 912468 != 4

GREEN: make it pass

I change the expectation to match reality

24        self.assertEqual(
25            src.calculator.add(123456, 789012),
26            912468
27        )

the test passes

RED: make it fail

I add another test, this time with a negative number

24        self.assertEqual(
25            src.calculator.add(123456, 789012),
26            912468
27        )
28        self.assertEqual(
29            src.calculator.add(-1, 0),
30            912468
31        )

the terminal shows AssertionError

AssertionError: -1 != 912468

GREEN: make it pass

I change the expectation to match reality

28    self.assertEqual(
29        src.calculator.add(-1, 0),
30        -1
31    )

the test passes

RED: make it fail

I try another test with two negative numbers

28        self.assertEqual(
29            src.calculator.add(-1, 0),
30            -1
31        )
32        self.assertEqual(
33            src.calculator.add(-2, -3),
34            -1
35        )

the terminal shows AssertionError

AssertionError: -5 != -1

GREEN: make it pass

I make the expectation match reality

32        self.assertEqual(
33            src.calculator.add(-2, -3),
34            -5
35        )

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

RED: make it fail

I add another test with floats (binary floating point decimal numbers)

32          self.assertEqual(
33              src.calculator.add(-2, -3),
34              -5
35          )
36          self.assertEqual(
37              src.calculator.add(0.1234, -5.6789),
38              -5
39          )

the terminal shows AssertionError

AssertionError: -5.555499999999999 != -5

GREEN: make it pass

I change the expectation

36          self.assertEqual(
37              src.calculator.add(0.1234, -5.6789),
38              -5.555499999999999
39          )

the test passes. Why is the result -5.555499999999999 instead of 5.5555?

what is a variable?

I just did the same kind of calculation 8 times in a row, there is a better way to this, you have seen this in Algebra. I can use a letter or a name for the numbers, that way I can have one test which covers all possible numbers. I can do this with 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(-1, 0) is -1+0 is -1
src.calculator.add(-2, -3) is -2+-3 is -5
src.calculator.add(0.1234, -5.6789) is 0.1234+-5.6789 is -5.555499999999999

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

src.calculator.add(first_number, second_number) is first_number+second_number
  • I add a new test at the beginning of test_addition with names for the values

     5class TestCalculator(unittest.TestCase):
     6
     7    def test_addition(self):
     8        self.assertEqual(
     9            src.calculator.add(first_number, second_number),
    10            first_number+second_number
    11        )
    12        self.assertEqual(
    13            src.calculator.add(0, 1),
    14            1
    15        )
    

    the terminal shows NameError

    NameError: name 'first_number' is not defined
    

    I have to tell Python what the value of first_number is

  • I point first_number to 0 before the test

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

    the terminal shows NameError

    NameError: name 'second_number' is not defined
    

    I have to tell Python what the value of second_number is

  • I point second_number to 1 before the test

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

    the test passes

  • I remove the next test since it is now covered by this new assertion that uses a variable

    11    self.assertEqual(
    12        src.calculator.add(first_number, second_number),
    13        first_number+second_number
    14    )
    15    self.assertEqual(
    16        src.calculator.add(0, 2),
    17        2
    18    )
    
  • I can do the same thing for all the other tests by changing the values for first_input and second_input, for example

    8        first_number = 0
    9        second_number = 2
    

    the test is still green

    8        first_number = 1
    9        second_number = 3
    

    still green

    8        first_number = 123456
    9        second_number = 789012
    

    the terminal still shows green

    8        first_number = 0.1234
    9        second_number = -5.6789
    

    the test is still passing. The problem with this is I lose the test for the previous number, everytime I change a number. I need a better way

  • I want to use random numbers for first_input and second_input to make sure that the add function always returns the result of adding the two numbers without knowing what the numbers will be. I can do this with the random module from the Python standard library. I add an import statement for it at the top of test_calculator.py

    1import random
    2import src.calculator
    3import unittest
    

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

  • I use a random value for first_number

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

    the test is still green. random.triangular returns a random float that could be any number from -0.1 to 1.0 in this case

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

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

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

    AssertionError: -X.YZABCDEFGHIJKLM != A.BCDEFGHIJKLMNOPQ
    
  • I change the expectation of the assertion back to the right calculation

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

    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
    12        self.assertEqual(
    13            src.calculator.add(first_number, second_number),
    14            first_number+second_number
    15        )
    16
    17
    18# TODO
    

    still green

  • I change the name of the variables to be more clear

     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    )
    

    the test is still green

  • 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, 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)
    

    the test is 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):
    

    then 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 terminal still shows 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 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)
    

    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.triangular(-10.0**100000, 10.0**100000)
    

    the terminal shows OverflowError

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

    because the numbers are too big to be used

    • ** 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

  • 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

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
    

    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 AssertionError with random values that look like this

    the terminal shows AssertionError

    AssertionError: None != XYZ.ABCDEFGHIJKLMNOP
    

    subtract returns None, the test expects random_first_number-random_second_number or 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!

REFACTOR: make it better

  • I have some duplication to remove, the code below happens twice in test_calculator.py

    random_first_number = a_random_number()
    random_second_number = 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    random_first_number = a_random_number()
    13    random_second_number = a_random_number()
    14
    15    def test_addition(self):
    16        random_first_number = a_random_number()
    17        random_second_number = a_random_number()
    
  • 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(
    22            src.calculator.add(
    23                random_first_number,
    24                random_second_number
    25            ),
    26            random_first_number+random_second_number
    27        )
    

    and in test_subtraction

    29    def test_subtraction(self):
    30        # random_first_number = a_random_number()
    31        random_first_number = self.random_first_number
    32        # random_second_number = a_random_number()
    33        random_second_number = self.random_second_number
    34
    35        self.assertEqual(
    36            src.calculator.subtract(
    37                random_first_number,
    38                random_second_number
    39            ),
    40            random_first_number-random_second_number
    41        )
    

    the terminal shows the tests are still passing. 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 methods like assertEqual or assertFalse

  • I remove the commented lines in test_addition

    15    def test_addition(self):
    16        random_first_number = self.random_first_number
    17        random_second_number = self.random_second_number
    

    and do the same thing in test_subtraction

    27    def test_subtraction(self):
    28        random_first_number = self.random_first_number
    29        random_second_number = self.random_second_number
    
  • I can use the class attributes directly in test_addition

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

    the test is still green. I remove the commented lines

    19        self.assertEqual(
    20            src.calculator.add(
    21                self.random_first_number,
    22                self.random_second_number
    23            ),
    24            self.random_first_number+self.random_second_number
    25        )
    
  • I do the same thing in test_subtraction

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

    the test is still passing. I remove the commented lines

    31        self.assertEqual(
    32            src.calculator.subtract(
    33                self.random_first_number,
    34                self.random_second_number
    35            ),
    36            self.random_first_number-self.random_second_number
    37        )
    
  • I remove the first_random_number and second_random_number variables from test_addition and test_subtraction because they are no longer used

    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):
    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):
    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
    34# TODO
    

    and the tests are still green!

  • I remove test subtraction from the TODO list

    34# TODO
    35# test multiplication
    36# test division
    37
    38
    39# 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        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):
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
43# 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

43# TODO
44# test division
45
46
47# Exceptions seen
  • * 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

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):
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# TODO

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

    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
    57# ZeroDivisionError
    

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 4 failures, 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

    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 variables 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, 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
    

    when I change the return statement to match the expectation

     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 the file(s) I have open in the editor(s)

  • I click in the terminal and exit the tests with ctrl+c on the keyboard

  • I deactivate the virtual environment

    deactivate
    

    the terminal goes back to the command line, (.venv) is no longer on the left side

    .../pumping_python/calculator
    
  • 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 the following tests for a program that can add, subtract, multiply and divide

I also saw the following 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

There is a problem, I have done the same steps for each of the 8 chapters covered so far

I think I know how to make a Python Test Driven Development environment. I am going to write a program that will do all the steps for making a project for me, so I never have to do those steps again.

Would you like to know how to make a Python Test Driven Development environment automatically?