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) oroption
(mac) on the keyboard and use the mouse to click ontests/test_calculator.py:7
to open it in the editor in the editorthen change
True
toFalse
to make the test passI 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
totest_addition
then add an assertionimport 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 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
, because0
plus1
is1
but the terminal shows NameError
NameError: name 'src' is not defined
because
src
is not defined intest_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 emptycalculator.py
file from thesrc
folder/directoryI add the error to the list of Exceptions encountered
# Exceptions Encountered # AssertionError # NameError # AttributeError
then open
calculator.py
in the editor to put the nameadd
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
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 callabledef 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
and1
I make it take 2 positional arguments
def add(x, y): return None
and get AssertionError
AssertionError: None != 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 calledx
to the result of callingrandom.randint(-1, 1)
which gives me a random number from-1
up to and including1
-1
for negative numbers,0
for itself, and1
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
andy
variablesdef 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 expectsx-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 intest_subtraction
. I can use class attributes (variables) to remove them and use the same numbers for both testsclass 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
andy
variables are made once as class attributes (variables) and used later in each test withself.x
andself.y
, the same way I use unittest.TestCase methods like assertEqual or assertFalseI remove the
x
andy
variables fromtest_addition
andtest_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 randomly0
the terminal shows a ZeroDivisionErrorx = 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 PythonI 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 be0
, I use a while statement to make sure it never happens in the assertiondef 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
is0
it points
self.y
to the result of callinga_random_number
then checks if the value of
self.y
is0
again, repeating the process untilself.y
is not0
if the value of
self.y
is not0
at any point, it exits the loop and runs the code in theelse
block
Since
self.y
is0
in the first part of the while statement I can add a call to thedivide
function that will faildef 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 randomly0
, the terminal shows a ZeroDivisionErrorZeroDivisionError: 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 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.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 inputsI 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?