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
calculatoras 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) oroption(mac) on the keyboard and use the mouse to click ontests/test_calculator.py:7to open it in the editorthen I change
TruetoFalseto make the test pass7 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_failuretotest_additionthen change assertFalse to assertEqual1import 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 == yor askingis 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 myexpectation-1, because0plus1is1
NameError: name 'src' is not defined
because
srcis not defined intest_calculator.py
green: make it pass¶
I add the error to the list of Exceptions encountered in
test_calculator.py20# 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.addas an address,addis something (an attribute) in the emptycalculator.pyfile from thesrcfolder/directoryI add the error to the list of Exceptions encountered in
test_calculator.py21# Exceptions Encountered 22# AssertionError 23# NameError 24# AttributeError
then I click on
calculator.pyin thesrcfolder to open it in the editor, and I type the name1add
NameError: name 'add' is not defined
I point it to None
1add = None
TypeError: 'NoneType' object is not callable
I add the error to the list of Exceptions encountered in
test_calculator.py21# Exceptions Encountered 22# AssertionError 23# NameError 24# AttributeError 25# TypeError
then I change
addto a function to make it callable with the def keyword incalculator.pyNote
the line numbers below are a guide, you do not need to copy them
1def add(): 2 return None
TypeError: add() takes 0 positional arguments but 2 were given
the definition of
adddoes not take input, but 2 were given in the callsrc.calculator.add(0, 1)-0and1I make it take 2 arguments
1def add(x, y): 2 return None
the terminal shows AssertionError
AssertionError: None != 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.pyto use random numbers in the test1import 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) orcommand+s(mac)) a few times in the editor to run the tests and the terminal shows random success or AssertionErrorAssertionError: 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-1up to and including1-1for negative numbers0for01for 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
xandyvariables intest_addition12 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**100000is how to write10raised to the power of100,000. I change the range back to-10, 10to keep the test running fast6def a_random_number(): 7 return random.randint(-10, 10)
then I remove
test additionfrom the TODO list22# 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.py10class 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.pyNote
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
NameError: name 'subtract' is not defined
I point
subtractto None5subtract = None
TypeError: 'NoneType' object is not callable
I have seen this before
I change
subtractto a function to make it callable5def subtract(): 6 return None
TypeError: subtract() takes 0 positional arguments but 2 were given
I make
subtracttake inputs5def subtract(x, y): 6 return None
the terminal shows AssertionError
AssertionError: None != -17 AssertionError: None != -4 AssertionError: None != 7 AssertionError: None != 10
subtractreturns None, the test expectsx-yI make the
subtractfunction return the difference between the inputs5def 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_additionand again intest_subtraction. I add class attributes (variables) to remove the duplication and use the same numbers for both tests10class TestCalculator(unittest.TestCase): 11 12 x = a_random_number() 13 y = a_random_number()
I use the new class attributes in
test_addition15 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_subtraction24 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
xandyvariables are made once as class attributes (variables) and used later in each test withself.xandself.y, the same way I use unittest.TestCase methods like assertEqual or assertFalseI can use the class attributes directly in
test_addition19 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_subtraction28 self.assertEqual( 29 src.calculator.subtract(self.x, self.y), 30 self.x-self.y 31 )
I remove the
xandyvariables fromtest_additionandtest_subtractionsince they are no longer needed10class 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 subtractionfrom the TODO list28# 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.py9def 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.py6def a_random_number(): 7 return random.randint(-1, 1)
I hit save (
ctrl+s(windows/linux) orcommand+s(mac)) a few times to run the tests, and whenyis randomly0the terminal shows ZeroDivisionErrorx = -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
0is undefined in mathematics and raises ZeroDivisionError in PythonI add it to the list of Exceptions encountered in
test_calculator.py44# 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
033 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.ycan sometimes be0, I use a while statement to make a never ending loop to make sure it never happens in the assertion intest_calculator.py33 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.yis0it points
self.yto the result of callinga_random_number()then it checks if the value of
self.yis0again. The process happens again non stop untilself.yis not0
when the value of
self.yis not0, it leaves the while loop and runs the code in theelseblock
Since
self.yis0in the first part of the while statement I can add a call to thedividefunction that will fail33 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) orcommand+s(mac)) in the editor a few times to run the tests, and whenself.yis randomly0, the terminal shows ZeroDivisionErrorZeroDivisionError: 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, 10to keep the tests fast6def 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.pythen delete all the text in
calculator.py, the terminal shows AttributeErrorAttributeError: 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.py1subtract
NameError: name 'subtract' is not defined
I point it to None
1subtract = None
TypeError: 'NoneType' object is not callable
I change
subtractto a function1def subtract(): 2 return None
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
subtractand the test expects the difference between the 2 inputsI 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
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