how to make a calculator part 5
I want to use the things I know to make the tests for the calculator program better
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(-1000.0, 1000.0)
8
9
10class 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):
17 self.assertEqual(
18 src.calculator.add(
19 self.random_first_number,
20 self.random_second_number
21 ),
22 self.random_first_number+self.random_second_number
23 )
24
25 def test_subtraction(self):
26 self.assertEqual(
27 src.calculator.subtract(
28 self.random_first_number,
29 self.random_second_number
30 ),
31 self.random_first_number-self.random_second_number
32 )
33
34 def test_multiplication(self):
35 self.assertEqual(
36 src.calculator.multiply(
37 self.random_first_number,
38 self.random_second_number
39 ),
40 self.random_first_number*self.random_second_number
41 )
42
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 'undefined: I cannot divide by 0'
56 )
57
58 def test_calculator_sends_message_when_input_is_not_a_number(self):
59 error_message = 'Excuse me?! Numbers only. Try again...'
60
61 for data in (
62 None,
63 True, False,
64 str(),
65 tuple(),
66 list(),
67 set(),
68 dict(),
69 ):
70 with self.subTest(i=data):
71 self.assertEqual(
72 src.calculator.add(data, a_random_number()),
73 error_message
74 )
75 self.assertEqual(
76 src.calculator.divide(data, a_random_number()),
77 error_message
78 )
79 self.assertEqual(
80 src.calculator.multiply(data, a_random_number()),
81 error_message
82 )
83 self.assertEqual(
84 src.calculator.subtract(data, a_random_number()),
85 error_message
86 )
87
88 def test_calculator_w_list_items(self):
89 a_list = [self.random_first_number, self.random_second_number]
90
91 self.assertEqual(
92 src.calculator.add(a_list[0], a_list[1]),
93 self.random_first_number+self.random_second_number
94 )
95 self.assertEqual(
96 src.calculator.divide(a_list[-2], a_list[-1]),
97 self.random_first_number/self.random_second_number
98 )
99 self.assertEqual(
100 src.calculator.multiply(a_list[1], a_list[-1]),
101 self.random_second_number*self.random_second_number
102 )
103 self.assertEqual(
104 src.calculator.subtract(a_list[-2], a_list[0]),
105 self.random_first_number-self.random_first_number
106 )
107 self.assertEqual(
108 src.calculator.add(*a_list),
109 self.random_first_number+self.random_second_number
110 )
111 self.assertEqual(
112 src.calculator.divide(*a_list),
113 self.random_first_number/self.random_second_number
114 )
115 self.assertEqual(
116 src.calculator.multiply(*a_list),
117 self.random_first_number*self.random_second_number
118 )
119 self.assertEqual(
120 src.calculator.subtract(*a_list),
121 self.random_first_number-self.random_second_number
122 )
123
124 def test_calculator_raises_type_error_when_given_more_than_two_inputs(self):
125 not_two_numbers = [0, 1, 2]
126
127 with self.assertRaises(TypeError):
128 src.calculator.add(*not_two_numbers)
129 with self.assertRaises(TypeError):
130 src.calculator.divide(*not_two_numbers)
131 with self.assertRaises(TypeError):
132 src.calculator.multiply(*not_two_numbers)
133 with self.assertRaises(TypeError):
134 src.calculator.subtract(*not_two_numbers)
135
136
137# Exceptions seen
138# AssertionError
139# NameError
140# AttributeError
141# TypeError
142# ZeroDivisionError
143# SyntaxError
open the project
I change directory to the
calculatorfoldercd calculatorthe terminal shows I am in the
calculatorfolder.../pumping_python/calculatorI activate the virtual environment
source .venv/bin/activateAttention
on Windows without Windows Subsystem for Linux use
.venv/bin/activate.ps1NOTsource .venv/bin/activate.venv/scripts/activate.ps1the terminal shows
(.venv) .../pumping_python/calculatorI use
pytest-watchto run the testspytest-watchthe terminal shows
rootdir: .../pumping_python/calculator collected 7 items tests/test_calculator.py ....... [100%] ============================ 7 passed in X.YZs =============================I hold ctrl on the keyboard and 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 had 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
REFACTOR: make it better
I add the setUp method to the TestCalculator class
10class 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 is still green. The setUp method runs before every test, giving random_first_number and random_second_number new random values for each test
a better way to test the calculator with inputs that are NOT numbers
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 for each data type
RED: make it fail
I add a new assertion to test_calculator_sends_message_when_input_is_not_a_number
58 def test_calculator_sends_message_when_input_is_not_a_number(self):
59 error_message = 'Excuse me?! Numbers only! try again...'
60
61 for data in (
62 None,
63 True, False,
64 str(),
65 tuple(),
66 list(),
67 set(),
68 dict(),
69 ):
70 self.assertEqual(
71 src.calculator.add(data, a_random_number()),
72 'BOOM!!!'
73 )
74
75 self.assertEqual(
76 src.calculator.add(None, None),
77 error_message
78 )
the terminal shows AssertionError
AssertionError: 'Excuse me?! Numbers only! try again...' != 'BOOM!!!'
Lovely! The if statement in the only_takes_numbers function in calculator.py is doing its job, the calculator only takes numbers
GREEN: make it pass
I change the expectation to match
70 self.assertEqual( 71 src.calculator.add(data, a_random_number()), 72 error_message 73 )the terminal shows AssertionError
AssertionError: ABC.DEFGHIJKLMNOPQR != 'Excuse me?! Numbers only! try again...'there is a problem. One of the data types I am testing is being allowed by the if statement, which means one of them is also an integer or is a float. I need a way to know which one is causing the problem
the unittest.TestCase class has a way to tell which item is causing my failure when I am using a loop, I add it to the test
68 dict(), 69 ): 70 with self.subTest(i=data): 71 self.assertEqual( 72 src.calculator.add(data, a_random_number()), 73 error_message 74 ) 75 76 self.assertEqual( 77 src.calculator.add(None, None), 78 error_message 79 )the terminal shows AssertionError for two of the data types I am testing
tests/test_calculator.py:72: AssertionError ============= short test summary info ============== SUBFAILED(i=True) tests/test_calculator.py::TestCalculator::test_calculator_sends_message_when_input_is_not_a_number - AssertionError: UVW.XYZABCDEFGHIJKL != 'Excuse ... SUBFAILED(i=False) tests/test_calculator.py::TestCalculator::test_calculator_sends_message_when_input_is_not_a_number - AssertionError: MNO.PQRSTUVWXYZABCD != 'Excuse ... =========== 2 failed, 7 passed in X.YZs ============the unittest.TestCase.subTest method runs the code in its context as a sub test, showing the values I give in
i=dataso that I can see which one caused the errorI add a condition for booleans in the
only_takes_numbersfunction incalculator.py4 error_message = 'Excuse me?! Numbers only! try again...' 5 6 if isinstance(first_input, bool) or isinstance(second_input, bool): 7 return error_message 8 if not (isinstance(first_input, good_types) and isinstance(second_input, good_types)):the test passes
REFACTOR: make it better
I can use a for loop to make the if statements simpler
1def only_takes_numbers(function): 2 def wrapper(first_input, second_input): 3 good_types = (int, float) 4 error_message = 'Excuse me?! Numbers only. Try again...' 5 6 # if isinstance(first_input, bool) or isinstance(second_input, bool): 7 # return error_message 8 # if not (isinstance(first_input, good_types) and isinstance(second_input, good_types)): 9 # return error_message 10 11 for value in (first_input, second_input): 12 if isinstance(value, bool) or not isinstance(value, good_types): 13 return error_message 14 15 try: 16 return function(first_input, second_input) 17 except TypeError: 18 return error_message 19 return wrapperthe test is still green
I remove the commented lines
1def only_takes_numbers(function): 2 def wrapper(first_input, second_input): 3 good_types = (int, float) 4 error_message = 'Excuse me?! Numbers only. Try again...' 5 6 for value in (first_input, second_input): 7 if isinstance(value, bool) or not isinstance(value, good_types): 8 return error_message 9 10 try: 11 return function(first_input, second_input) 12 except TypeError: 13 return error_message 14 return wrapperstill green
I add another assertion for the divide function in
test_calculator.py70 with self.subTest(i=data): 71 self.assertEqual( 72 src.calculator.add(data, a_random_number()), 73 error_message 74 ) 75 self.assertEqual( 76 src.calculator.divide(data, a_random_number()), 77 'BOOM!!!' 78 )the terminal shows AssertionError for all the data types in the test
AssertionError: 'Excuse me?! Numbers only! try again...' != 'BOOM!!!'I think you can tell what will happen next. I change the expectation to match
75 self.assertEqual( 76 src.calculator.divide(data, a_random_number()), 77 error_message 78 )the test passes
I add an assertion for multiplication
75 self.assertEqual( 76 src.calculator.divide(data, a_random_number()), 77 error_message 78 ) 79 self.assertEqual( 80 src.calculator.multiply(data, a_random_number()), 81 'BOOM!!!' 82 )the terminal shows AssertionError for each data type
AssertionError: 'Excuse me?! Numbers only! try again...' != 'BOOM!!!'I change the expectation
79 self.assertEqual( 80 src.calculator.multiply(data, a_random_number()), 81 error_message 82 )the test passes
I add an assertion for subtraction
79 self.assertEqual( 80 src.calculator.multiply(data, a_random_number()), 81 error_message 82 ) 83 self.assertEqual( 84 src.calculator.subtract(data, a_random_number()), 85 'BOOM!!!' 86 )the terminal shows AssertionError for all the data types I am testing
AssertionError: 'Excuse me?! Numbers only! try again...' != 'BOOM!!!'I change the expectation to match
83 self.assertEqual( 84 src.calculator.subtract(data, a_random_number()), 85 error_message 86 )the test passes
I remove the other assertions in the test because they are now covered by the for loop
58 def test_calculator_sends_message_when_input_is_not_a_number(self): 59 error_message = 'Excuse me?! Numbers only! try again...' 60 61 for data in ( 62 None, 63 True, False, 64 str(), 65 tuple(), 66 list(), 67 set(), 68 dict(), 69 ): 70 with self.subTest(i=data): 71 self.assertEqual( 72 src.calculator.add(data, a_random_number()), 73 error_message 74 ) 75 self.assertEqual( 76 src.calculator.divide(data, a_random_number()), 77 error_message 78 ) 79 self.assertEqual( 80 src.calculator.multiply(data, a_random_number()), 81 error_message 82 ) 83 self.assertEqual( 84 src.calculator.subtract(data, a_random_number()), 85 error_message 86 ) 87 88 def test_calculator_w_list_items(self):Using a for loop saved me having to write a lot of tests
I can add more data to the iterable without having to add more tests
58 def test_calculator_sends_message_when_input_is_not_a_number(self): 59 error_message = 'Excuse me?! Numbers only! try again...' 60 61 for data in ( 62 None, 63 True, False, 64 str(), 'text', 65 tuple(), (0, 1, 2, 'n'), 66 list(), [0, 1, 2, 'n'], 67 set(), {0, 1, 2, 'n'}, 68 dict(), {'key': 'value'}, 69 ): 70 with self.subTest(i=data):the test is still green
I could also write the test with a list comprehension, though it looks ugly
84 self.assertEqual( 85 src.calculator.subtract(data, a_random_number()), 86 error_message 87 ) 88 89 [ 90 self.assertEqual( 91 src.calculator.add(data, a_random_number), 92 'BOOM!!!' 93 ) for data in ( 94 None, True, False, str(), 'text', 95 tuple(), (0, 1, 2, 'n'), 96 list(), [0, 1, 2, 'n'], 97 set(), {0, 1, 2, 'n'}, 98 dict(), {'key': 'value'}, 99 ) 100 ] 101 102def test_calculator_w_list_items(self):the terminal shows AssertionError
AssertionError: 'Excuse me?! Numbers only! try again...' != 'BOOM!!!'There are a few problems with doing it this way
I make a list when I do not need it
I would not have been able to tell which data type failed since I cannot use the subTest method with this
I would have to repeat all those lines for each function in the calculator program
I remove it from the test and things are green again
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 and exit the tests with ctrl+c on the keyboard, the terminal shows
(.venv) .../pumping_python/calculatorI deactivate the virtual environment
deactivatethe terminal goes back to the command line,
(.venv)is no longer on the left side.../pumping_python/calculatorI 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 the following
{
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 leave a 5 star review. It helps other people get into the book too