how to make a calculator 6
I want to use the things I know to make the tests for the calculator program better
preview
These are the tests I have by the end of the chapter
open the project
I change directory to the
calculatorfoldercd calculatorthe terminal shows I am in the
calculatorfolder.../pumping_python/calculatorI use
pytest-watcherto run the testsuv run pytest-watcher . --nowthe terminal shows
rootdir: .../pumping_python/calculator configfile: pyproject.toml collected 7 items tests/test_calculator.py ....... [100%] ======================== 7 passed in X.YZs =========================I hold ctrl on the keyboard, then click on
tests/test_calculator.pyto open it in the editor
how to make sure the calculator tests use new numbers for every test
I used the setUp method in list comprehensions to make sure that I have a new list and iterable for every test. I want to do the same thing with the calculator, to make sure that each test uses 2 new different random numbers, not the same random numbers for every test
RED: make it fail
I add the setUp method to the TestCalculator class
10class TestCalculator(unittest.TestCase):
11
12 def setUp(self):
13 random_first_number = a_random_number()
14 random_second_number = a_random_number()
15
16 def test_addition(self):
the terminal shows AttributeError
AttributeError: 'TestCalculator' object has no attribute 'random_first_number'
random_first_number and random_second_number can no longer be reached because they belong to the setUp method, I have to make sure they are class attributes
GREEN: make it pass
I add
self.torandom_first_number12 def setUp(self): 13 self.random_first_number = a_random_number() 14 random_second_number = a_random_number()the terminal shows AttributeError
AttributeError: 'TestCalculator' object has no attribute 'random_second_number'. Did you mean: 'random_first_number'?I add
self.torandom_second_number10class TestCalculator(unittest.TestCase): 11 12 def setUp(self): 13 self.random_first_number = a_random_number() 14 self.random_second_number = a_random_number() 15 16 def test_addition(self):the test passes.
The setUp method runs before every test, giving random_first_number and random_second_number new random values for each test.
test_calculator_w_a_for_loop
I tested the calculator functions with None, strings and lists, I want to test them with the other basic Python data types: booleans, tuples, sets and dictionaries.
Since I know how to use a for loop and list comprehensions, I can do this with one test for all of them instead of a different test with 4 or more assertions for each data type
RED: make it fail
I add a new test with a for loop and an assertion to test_calculator.py
164 def test_calculator_raises_type_error_when_given_more_than_two_inputs(self):
165 not_two_numbers = [0, 1, 2]
166
167 with self.assertRaises(TypeError):
168 src.calculator.add(*not_two_numbers)
169 with self.assertRaises(TypeError):
170 src.calculator.divide(*not_two_numbers)
171 with self.assertRaises(TypeError):
172 src.calculator.multiply(*not_two_numbers)
173 with self.assertRaises(TypeError):
174 src.calculator.subtract(*not_two_numbers)
175
176 def test_calculator_w_a_for_loop(self):
177 error_message = 'brmph?! Numbers only. Try again...'
178
179 for data_type in (
180 None,
181 True, False,
182 str(),
183 tuple(),
184 list(),
185 set(),
186 dict(),
187 ):
188 self.assertEqual(
189 src.calculator.add(
190 data_type, a_random_number()
191 ),
192 'BOOM!!!'
193 )
194
195
196# Exceptions seen
the terminal shows AssertionError
AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!'s
GREEN: make it pass
I change the expectation to match
188 self.assertEqual( 189 src.calculator.add( 190 data_type, a_random_number() 191 ), 192 error_message 193 )the terminal shows AssertionError
AssertionError: ABC.DEFGHIJKLMNOPQR != 'brmph?! Numbers only. Try again...'One of the data types from the test gets to the add function so it returns a number and not a message. How can I tell which data type caused the failure?
the unittest.TestCase class has a way to tell which item is causing my failure when I am using a for loop, I add it to the test
176 def test_calculator_w_a_for_loop(self): 177 error_message = 'brmph?! Numbers only. Try again...' 178 179 for data_type in ( 180 None, 181 True, False, 182 str(), 183 tuple(), 184 list(), 185 set(), 186 dict(), 187 ): 188 with self.subTest(data_type=data_type): 189 self.assertEqual( 190 src.calculator.add( 191 data_type, a_random_number() 192 ), 193 error_message 194 ) 195 196# Exceptions seenthe terminal shows AssertionError for two of the data types I am testing
tests/test_calculator.py:189: AssertionError ============= short test summary info ============== SUBFAILED(data_type=True) ... - AssertionError: UVW.XYZABCDEFGHIJKL != 'brmph?! Numbers only. Try again...' SUBFAILED(data_type=False) ... - AssertionError: MNO.PQRSTUVWXYZABCD != 'brmph?! Numbers only. Try again...' =========== 2 failed, 7 passed in X.YZs ============the unittest.TestCase.subTest method runs the code under it as a sub test, showing the values I give in
data_type=data_typeso that I can see which one caused the error. In this case the failures were caused by True and False. Is a boolean a number?then add a condition for booleans to the add function in
calculator.py38@numbers_only 39def add(first_input, second_input): 40 if ( 41 isinstance(first_input, str) 42 or 43 isinstance(second_input, str) 44 ): 45 return 'brmph?! Numbers only. Try again...' 46 if ( 47 isinstance(first_input, bool) 48 or 49 isinstance(second_input, bool) 50 ): 51 return 'brmph?! Numbers only. Try again...' 52 return first_input + second_inputthe test passes
how to test if something is an instance of more than one type
The two if statements in the add function look the same
if (
isinstance(first_input, data_type)
or
isinstance(second_input, data_type)
):
return 'brmph?! Numbers only. Try again...'
the only difference are the data types
isinstance(something, str)
isinstance(something, bool)
The isinstance function can take a tuple as the second input, which means I can if the first input is an instance of any of the objects in the tuple
I add a new if statement to the add function
38@numbers_only 39def add(first_input, second_input): 40 # if ( 41 # isinstance(first_input, str) 42 # or 43 # isinstance(second_input, str) 44 # ): 45 # return 'brmph?! Numbers only. Try again...' 46 # if ( 47 # isinstance(first_input, bool) 48 # or 49 # isinstance(second_input, bool) 50 # ): 51 if ( 52 isinstance(first_input, (str, bool)) 53 or 54 isinstance(second_input, (str, bool)) 55 ): 56 return 'brmph?! Numbers only. Try again...' 57 return first_input + second_inputthe test is still green
I remove the other if statements
38@numbers_only 39def add(first_input, second_input): 40 if ( 41 isinstance(first_input, (str, bool)) 42 or 43 isinstance(second_input, (str, bool)) 44 ): 45 return 'brmph?! Numbers only. Try again...' 46 return first_input + second_inputstill green
I add an assertion for the divide function to test_calculator_w_a_for_loop in
test_calculator.py176 def test_calculator_w_a_for_loop(self): 177 error_message = 'brmph?! Numbers only. Try again...' 178 179 for data_type in ( 180 None, 181 True, False, 182 str(), 183 tuple(), 184 list(), 185 set(), 186 dict(), 187 ): 188 with self.subTest(data_type=data_type): 189 self.assertEqual( 190 src.calculator.add( 191 data_type, a_random_number() 192 ), 193 error_message 194 ) 195 self.assertEqual( 196 src.calculator.divide( 197 data_type, a_random_number() 198 ), 199 'BOOM!!!' 200 )the terminal shows AssertionError
===================== short test summary info ====================== SUBFAILED(data_type=None) ... - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!' SUBFAILED(data_type=True) ... - AssertionError: -A.BCDEFGHIJKLMNOPQRS != 'BOOM!!!' SUBFAILED(data_type=False) ...- AssertionError: -T.U != 'BOOM!!!' SUBFAILED(data_type='') ... - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!' SUBFAILED(data_type=()) ... - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!' SUBFAILED(data_type=[]) ... - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!' SUBFAILED(data_type=set()) ...- AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!' SUBFAILED(data_type={}) ... - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!' =================== 8 failed, 8 passed in V.WXs ====================I change the expectation to use the error message
195 self.assertEqual( 196 src.calculator.divide( 197 data_type, a_random_number() 198 ), 199 error_message 200 )the terminal shows AssertionError
SUBFAILED(data_type=True) ... - AssertionError: Y.ZABCDEFGHIJKLMNOP != 'brmph?! Numbers only. Try again...' SUBFAILED(data_type=False) ... - AssertionError: Q.R != 'brmph?! Numbers only. Try again...'I add an if statement to the divide function in
calculator.py30@numbers_only 31def divide(first_input, second_input): 32 if ( 33 isinstance(first_input, bool) 34 or 35 isinstance(second_input, bool) 36 ): 37 return 'brmph?! Numbers only. Try again...' 38 try: 39 return first_input / second_input 40 except ZeroDivisionError: 41 return 'brmph?! I cannot divide by 0. Try again...' 42 43 44@numbers_only 45def add(first_input, second_input):the test passes
I add an assertion for the multiply function in
test_calculator.py195 self.assertEqual( 196 src.calculator.divide( 197 data_type, a_random_number() 198 ), 199 error_message 200 ) 201 self.assertEqual( 202 src.calculator.multiply( 203 data_type, a_random_number() 204 ), 205 'BOOM!!!' 206 ) 207 208 209# Exceptions seenthe terminal shows AssertionError
SUBFAILED(data_type=None) ... - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!' SUBFAILED(data_type=True) ... - AssertionError: ST.UVWXYZABCDEFGHI != 'BOOM!!!' SUBFAILED(data_type=False) ... - AssertionError: J.K != 'BOOM!!!' SUBFAILED(data_type='') ... - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!' SUBFAILED(data_type=()) ... - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!' SUBFAILED(data_type=[]) ... - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!' SUBFAILED(data_type=set()) ... - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!' SUBFAILED(data_type={}) ... - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!'they look the same as with the divide function
I change the expectation of the assertion for multiplication
201 self.assertEqual( 202 src.calculator.multiply( 203 data_type, a_random_number() 204 ), 205 error_message 206 )the terminal shows AssertionError
SUBFAILED(data_type=True) ... - AssertionError: -LMN.OPQRSTUVWXYZAB != 'brmph?! Numbers only. Try again...' SUBFAILED(data_type=False) ... - AssertionError: -C.D != 'brmph?! Numbers only. Try again...'I add an if statement to the multiply function in
calculator.py19@numbers_only 20def multiply(first_input, second_input): 21 if ( 22 isinstance(first_input, list) 23 or 24 isinstance(second_input, list) 25 ): 26 return 'brmph?! Numbers only. Try again...' 27 if ( 28 isinstance(first_input, bool) 29 or 30 isinstance(second_input, bool) 31 ): 32 return 'brmph?! Numbers only. Try again...' 33 return first_input * second_input 34 35 36@numbers_only 37def divide(first_input, second_input):the test passes
I add a new if statement to put the two statements together
19@numbers_only 20def multiply(first_input, second_input): 21 # if ( 22 # isinstance(first_input, list) 23 # or 24 # isinstance(second_input, list) 25 # ): 26 # return 'brmph?! Numbers only. Try again...' 27 # if ( 28 # isinstance(first_input, bool) 29 # or 30 # isinstance(second_input, bool) 31 # ): 32 # return 'brmph?! Numbers only. Try again...' 33 if ( 34 isinstance(first_input, (list, bool)) 35 or 36 isinstance(second_input, (list, bool)) 37 ): 38 return 'brmph?! Numbers only. Try again...' 39 return first_input * second_inputthe test is still green
I remove the other if statements
19@numbers_only 20def multiply(first_input, second_input): 21 if ( 22 isinstance(first_input, (list, bool)) 23 or 24 isinstance(second_input, (list, bool)) 25 ): 26 return 'brmph?! Numbers only. Try again...' 27 return first_input * second_input 28 29 30@numbers_only 31def divide(first_input, second_input):still green
I add an assertion for the subtract function in
test_calculator.py201 self.assertEqual( 202 src.calculator.multiply( 203 data_type, a_random_number() 204 ), 205 error_message 206 ) 207 self.assertEqual( 208 src.calculator.subtract( 209 data_type, a_random_number() 210 ), 211 'BOOM!!!' 212 ) 213 214 215# Exceptions seenthe terminal shows AssertionError
SUBFAILED(data_type=None) ... - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!' SUBFAILED(data_type=True) ... - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!' SUBFAILED(data_type=False) ... - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!' SUBFAILED(data_type='') ... - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!' SUBFAILED(data_type=()) ... - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!' SUBFAILED(data_type=[]) ... - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!' SUBFAILED(data_type=set()) ... - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!' SUBFAILED(data_type={}) ... - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!'I change the expectation to use the error message
207 self.assertEqual( 208 src.calculator.subtract( 209 data_type, a_random_number() 210 ), 211 error_message 212 ) 213 214 215# Exceptions seenthe terminal shows AssertionError
SUBFAILED(data_type=True) ... - AssertionError: EFG.HIJKLMNOPQRSTU != 'brmph?! Numbers only. Try again...' SUBFAILED(data_type=False) ... - AssertionError: VWX.YZABCDEFGHIJK != 'brmph?! Numbers only. Try again...'I add an if statement to the subtract function in
calculator.py14@numbers_only 15def subtract(first_input, second_input): 16 if ( 17 isinstance(first_input, bool) 18 or 19 isinstance(second_input, bool) 20 ): 21 return 'brmph?! Numbers only. Try again...' 22 return first_input - second_input 23 24 25@numbers_only 26def multiply(first_input, second_input):the test passes. it looks like booleans are numbers
All 4 calculator functions have the same if statement
if ( isinstance(first_input, bool) or isinstance(second_input, bool) )the difference between them is that the
add function has
(str, bool)multiply function has
(list, bool)
I add the if statement to the
numbers_onlydecorator function to remove repetition1def numbers_only(function): 2 def decorator(first_input, second_input): 3 error_message = 'brmph?! Numbers only. Try again...' 4 if first_input is None or second_input is None: 5 return error_message 6 if ( 7 isinstance(first_input, (str, list, bool)) 8 or 9 isinstance(second_input, (str, list, bool)) 10 ): 11 return error_message 12 13 try: 14 return function(first_input, second_input) 15 except TypeError: 16 return error_message 17 return decorator 18 19 20@numbers_only 21def subtract(first_input, second_input):the tests are still green
I remove the if statement from the subtract function
21@numbers_only 22def subtract(first_input, second_input): 23 return first_input - second_input 24 25 26@numbers_only 27def multiply(first_input, second_input):still green
I remove the if statement from the multiply function
26@numbers_only 27def multiply(first_input, second_input): 28 return first_input * second_input 29 30 31@numbers_only 32def divide(first_input, second_input):green
I remove the if statement from the divide function
31@numbers_only 32def divide(first_input, second_input): 33 try: 34 return first_input / second_input 35 except ZeroDivisionError: 36 return 'brmph?! I cannot divide by 0. Try again...' 37 38 39@numbers_only 40def add(first_input, second_input):still green
I remove the if statement from the add function
39@numbers_only 40def add(first_input, second_input): 41 return first_input + second_inputthe tests are still green
I add a variable to the
numbers_onlydecorator function1def numbers_only(function): 2 def decorator(first_input, second_input): 3 bad_data_types = (str, list, bool) 4 error_message = 'brmph?! Numbers only. Try again...' 5 6 if first_input is None or second_input is None:I use the variable to remove the repetition of
(str, list, bool)6 if first_input is None or second_input is None: 7 return error_message 8 if ( 9 # isinstance(first_input, (str, list, bool)) 10 isinstance(first_input, bad_data_types) 11 or 12 # isinstance(second_input, (str, list, bool)) 13 isinstance(second_input, bad_data_types) 14 ): 15 return error_messagestill green
I remove the commented lines then use a for loop with the if statements
1def numbers_only(function): 2 def decorator(first_input, second_input): 3 bad_data_types = (str, list, bool) 4 error_message = 'brmph?! Numbers only. Try again...' 5 6 # if first_input is None or second_input is None: 7 # return error_message 8 # if ( 9 # isinstance(first_input, bad_data_types) 10 # or 11 # isinstance(second_input, bad_data_types) 12 # ): 13 # return error_message 14 15 for value in (first_input, second_input): 16 if value is None: 17 return error_message 18 if isinstance(value, bad_data_types): 19 return error_message 20 21 try: 22 return function(first_input, second_input) 23 except TypeError: 24 return error_message 25 return decoratorgreen
I remove the commented lines then put the two if statements together with logical disjunction
1def numbers_only(function): 2 def decorator(first_input, second_input): 3 bad_data_types = (str, list, bool) 4 error_message = 'brmph?! Numbers only. Try again...' 5 6 for value in (first_input, second_input): 7 # if value is None: 8 # return error_message 9 # if isinstance(value, bad_data_types): 10 # return error_message 11 12 if ( 13 value is None 14 or 15 isinstance(value, bad_data_types) 16 ): 17 return error_message 18 19 try: 20 return function(first_input, second_input) 21 except TypeError: 22 return error_message 23 return decoratorstill green
I remove the commented lines
1def numbers_only(function): 2 def decorator(first_input, second_input): 3 bad_data_types = (str, list, bool) 4 error_message = 'brmph?! Numbers only. Try again...' 5 6 for value in (first_input, second_input): 7 if ( 8 value is None 9 or 10 isinstance(value, bad_data_types) 11 ): 12 return error_message 13 14 try: 15 return function(first_input, second_input) 16 except TypeError: 17 return error_message 18 return decorator 19 20 21@numbers_only 22def subtract(first_input, second_input): 23 return first_input - second_input 24 25 26@numbers_only 27def multiply(first_input, second_input): 28 return first_input * second_input 29 30 31@numbers_only 32def divide(first_input, second_input): 33 try: 34 return first_input / second_input 35 except ZeroDivisionError: 36 return 'brmph?! I cannot divide by 0. Try again...' 37 38 39@numbers_only 40def add(first_input, second_input): 41 return first_input + second_inputthe tests are still green
I remove test_calculator_sends_message_when_input_is_not_a_number because it tests None, strings and lists, while test_calculator_w_a_for_loop tests None, booleans, strings, tuples, lists, sets and dictionaries
43 def test_division(self): 44 try: 45 self.assertEqual( 46 src.calculator.divide( 47 self.random_first_number, 48 self.random_second_number 49 ), 50 self.random_first_number/self.random_second_number 51 ) 52 except ZeroDivisionError: 53 self.assertEqual( 54 src.calculator.divide(self.random_first_number, 0), 55 'brmph?! I cannot divide by 0. Try again...' 56 ) 57 58 def test_calculator_w_list_items(self):I change the name of test_calculator_w_a_for_loop to test_calculator_sends_message_when_input_is_not_a_number
121 def test_calculator_sends_message_when_input_is_not_a_number(self): 122 error_message = 'brmph?! Numbers only. Try again...' 123 124 for data_type in ( 125 None, 126 True, False, 127 str(), 128 tuple(), 129 list(), 130 set(), 131 dict(), 132 ): 133 with self.subTest(data_type=data_type): 134 self.assertEqual( 135 src.calculator.add( 136 data_type, a_random_number() 137 ), 138 error_message 139 ) 140 self.assertEqual( 141 src.calculator.divide( 142 data_type, a_random_number() 143 ), 144 error_message 145 ) 146 self.assertEqual( 147 src.calculator.multiply( 148 data_type, a_random_number() 149 ), 150 error_message 151 ) 152 self.assertEqual( 153 src.calculator.subtract( 154 data_type, a_random_number() 155 ), 156 error_message 157 ) 158 159 160# Exceptions seen 161# AssertionError 162# NameError 163# AttributeError 164# TypeError
Using a for loop means I do not have to write a lot of tests. I can add more data to the iterable without having to add more tests
121 def test_calculator_sends_message_when_input_is_not_a_number(self): 122 error_message = 'brmph?! Numbers only. Try again...' 123 124 for data_type in ( 125 None, 126 True, False, 127 str(), 'text', 128 tuple(), (0, 1, 2, 'n'), 129 list(), [0, 1, 2, 'n'], 130 set(), {0, 1, 2, 'n'}, 131 dict(), {'key': 'value'}, 132 ): 133 with self.subTest(data_type=data_type):the test is still green
I can also write the test with a list comprehension, though it looks ugly
121 def test_calculator_sends_message_when_input_is_not_a_number(self): 122 error_message = 'brmph?! Numbers only. Try again...' 123 124 [ 125 self.assertEqual( 126 src.calculator.add(data_type, a_random_number()), 127 'BOOM!!!' 128 ) for data_type in ( 129 None, 130 True, False, 131 str(), 'text', 132 tuple(), (0, 1, 2, 'n'), 133 list(), [0, 1, 2, 'n'], 134 set(), {0, 1, 2, 'n'}, 135 dict(), {'key': 'value'}, 136 ) 137 ] 138 139 for data_type in ( 140 None, 141 True, False, 142 str(), 'text', 143 tuple(), (0, 1, 2, 'n'), 144 list(), [0, 1, 2, 'n'], 145 set(), {0, 1, 2, 'n'}, 146 dict(), {'key': 'value'}, 147 ):the terminal shows AssertionError
AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM!!!'I change the expectation to the error message
4 [ 5 self.assertEqual( 6 src.calculator.add(data_type, a_random_number()), 7 error_message 8 ) for data_type in ( 9 None, 10 True, False, 11 str(), 'text', 12 tuple(), (0, 1, 2, 'n'), 13 list(), [0, 1, 2, 'n'], 14 set(), {0, 1, 2, 'n'}, 15 dict(), {'key': 'value'}, 16 ) 17 ]the test passes. There are a few problems with doing it this way
it makes a list that is never used
I could tell which data type caused the failure since I cannot use the subTest method in the list comprehension
I would have to repeat all those lines for the other function in the calculator program
I know a better way to test the calculator with inputs that are NOT numbers
close the project
I close
test_calculator.pyandcalculator.pyin the editorsI click in the terminal, then use q on the keyboard to leave the tests. The terminal goes back to the command line
I change directory to the parent of
calculatorcd ..the terminal shows
.../pumping_pythonI am back in the
pumping_pythondirectory
review
I ran tests to show I can make a list from an iterable with
list comprehensions where I can
I can use functions and conditions with list comprehensions to make a list with one line. I think of it as
[
process(item)
for item in iterable
if condition/not condition
]
I can also do this with dictionaries, it is called a dict comprehension and the syntax is any mix of these
{
a_process(key): another_process(value)
for key/value in iterable
if condition/not condition
}
How many questions can you answer after going through this chapter?
code from the chapter
what is next?
you know
Would you like to test if a boolean is an integer or a float?
rate pumping python
If this has been a 7 star experience for you, please CLICK HERE to leave a 5 star review of pumping python. It helps other people get into the book too