how to make a calculator 7

I can use a dictionary to test the calculator functions as long as its values are numbers


open the project

  • I change directory to the calculator folder

    cd calculator
    

    the terminal shows I am in the calculator folder

    .../pumping_python/calculator
    
  • I activate the virtual environment

    source .venv/bin/activate
    

    Attention

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

    .venv/scripts/activate.ps1
    

    the terminal shows

    (.venv) .../pumping_python/calculator
    
  • I use pytest-watch to run the tests

    pytest-watch
    

    the 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.py to open it in the editor


test_calculator_w_dictionary_items

RED: make it fail

I add a new test to use a dictionary to test the calculator

119          self.assertEqual(
120              src.calculator.subtract(*a_list),
121              self.random_first_number-self.random_second_number
122          )
123
124      def test_calculator_w_dictionary_items(self):
125          two_numbers = {
126              'x': self.random_first_number,
127              'y': self.random_second_number,
128          }
129
130          self.assertEqual(
131              src.calculator.add(two_numbers['x'], two_numbers['y']),
132              self.random_first_number+self.random_first_number
133          )
134
135      def test_calculator_raises_type_error_when_given_more_than_two_inputs(self):
136          not_two_numbers = [0, 1, 2]

the terminal shows AssertionError

AssertionError: ABC.DEFGHIJKLMNOPQ != RST.UVWXYZABCDEFG

GREEN: make it pass

I change the expectation to the right calculation

130        self.assertEqual(
131            src.calculator.add(two_numbers['x'], two_numbers['y']),
132            self.random_first_number+self.random_second_number
133        )

the test passes

REFACTOR: make it better

  • I add an assertion for the divide function

    130        self.assertEqual(
    131            src.calculator.add(two_numbers['x'], two_numbers['y']),
    132            self.random_first_number+self.random_second_number
    133        )
    134        self.assertEqual(
    135            src.calculator.divide(two_numbers['x'], two_numbers['y']),
    136            self.random_first_number*self.random_second_number
    137        )
    

    the terminal shows AssertionError

    AssertionError: D.EFGHIJKLMNOPQRST != UVWXY.ZABCDEFGHIJ
    
  • I change the calculation

    134        self.assertEqual(
    135            src.calculator.divide(two_numbers['x'], two_numbers['y']),
    136            self.random_first_number/self.random_second_number
    137        )
    

    the test passes

  • I add another assertion

    134        self.assertEqual(
    135            src.calculator.divide(two_numbers['x'], two_numbers['y']),
    136            self.random_first_number/self.random_second_number
    137        )
    138        self.assertEqual(
    139            src.calculator.multiply(two_numbers['y'], two_numbers['y']),
    140            self.random_first_number*self.random_second_number
    141        )
    

    the terminal shows AssertionError

    AssertionError: EFGHIJ.KLMNOPQRSTU != VWXYZ.ABCDEFGHIJKL
    
  • I change the expectation

    123        self.assertEqual(
    124            src.calculator.multiply(two_numbers['y'], two_numbers['y']),
    125            self.random_second_number*self.random_second_number
    126        )
    

    the test passes

  • I add an assertion for the subtract function

    138        self.assertEqual(
    139            src.calculator.multiply(two_numbers['y'], two_numbers['y']),
    140            self.random_second_number*self.random_second_number
    141        )
    142        self.assertEqual(
    143            src.calculator.subtract(two_numbers['x'], two_numbers['x']),
    144            self.random_first_number-self.random_second_number
    145        )
    

    the terminal shows AssertionError

    AssertionError: 0.0 != FGH.IJKLMNOPQRSTU
    
  • I change the expectation to match

    142        self.assertEqual(
    143            src.calculator.subtract(two_numbers['x'], two_numbers['x']),
    144            self.random_first_number-self.random_first_number
    145        )
    

    the test passes

  • Python allows me use a double starred expression like I did in test_functions_w_unknown_arguments. I add an assertion with it

    142        self.assertEqual(
    143            src.calculator.subtract(two_numbers['x'], two_numbers['x']),
    144            self.random_first_number-self.random_first_number
    145        )
    146        self.assertEqual(
    147            src.calculator.add(**two_numbers),
    148            self.random_first_number-self.random_second_number
    149        )
    

    the terminal shows TypeError

    TypeError: only_takes_numbers.<locals>.wrapper() got an unexpected keyword argument 'x'
    
  • the names of the keys in the two_numbers dictionary must be the same as the names of the arguments the calculator functions receive - first_input and second_input not x and y. I change x and y to first_input and second_input in the test

    124    def test_calculator_w_dictionary_items(self):
    125        two_numbers = {
    126            'first_input': self.random_first_number,
    127            'second_input': self.random_second_number,
    128        }
    129
    130        self.assertEqual(
    131            src.calculator.add(
    132                two_numbers['first_input'],
    133                two_numbers['second_input']
    134            ),
    135            self.random_first_number+self.random_second_number
    136        )
    137        self.assertEqual(
    138            src.calculator.divide(
    139                two_numbers['first_input'],
    140                two_numbers['second_input']
    141            ),
    142            self.random_first_number/self.random_second_number
    143        )
    144        self.assertEqual(
    145            src.calculator.multiply(
    146                two_numbers['second_input'],
    147                two_numbers['second_input']
    148            ),
    149            self.random_second_number*self.random_second_number
    150        )
    151        self.assertEqual(
    152            src.calculator.subtract(
    153                two_numbers['first_input'],
    154                two_numbers['first_input']
    155            ),
    156            self.random_first_number-self.random_first_number
    157        )
    

    the terminal shows AssertionError

    AssertionError: VWX.YZABCDEFGHIJK != LMN.OPQRSTUVWXYZABC
    
  • I change the calculation in the assertion

    158        self.assertEqual(
    159            src.calculator.add(**two_numbers),
    160            self.random_first_number+self.random_second_number
    161        )
    

    the test passes

  • I add another assertion

    158        self.assertEqual(
    159            src.calculator.add(**two_numbers),
    160            self.random_first_number+self.random_second_number
    161        )
    162        self.assertEqual(
    163            src.calculator.divide(**two_numbers),
    164            self.random_first_number*self.random_second_number
    165        )
    

    the terminal shows AssertionError

    AssertionError: H.IJKLMNOPQRSTUVWX != YZABCD.EFGHIJKLMNO
    
  • I change the calculation

    162        self.assertEqual(
    163            src.calculator.divide(**two_numbers),
    164            self.random_first_number/self.random_second_number
    165        )
    

    the test passes

  • I add an assertion for the multiply function

    162        self.assertEqual(
    163            src.calculator.divide(**two_numbers),
    164            self.random_first_number/self.random_second_number
    165        )
    166        self.assertEqual(
    167            src.calculator.multiply(**two_numbers),
    168            self.random_first_number/self.random_second_number
    169        )
    

    the terminal shows AssertionError

    AssertionError: IJKLMN.OPQRSTUVWX != Y.ZABCDEFGHIJKLMNOP
    
  • I change the calculation

    166        self.assertEqual(
    167            src.calculator.multiply(**two_numbers),
    168            self.random_first_number*self.random_second_number
    169        )
    

    the test passes

  • I add the next assertion

    166        self.assertEqual(
    167            src.calculator.multiply(**two_numbers),
    168            self.random_first_number*self.random_second_number
    169        )
    170        self.assertEqual(
    171            src.calculator.subtract(**two_numbers),
    172            self.random_first_number+self.random_second_number
    173        )
    

    the terminal shows AssertionError

    AssertionError: JKL.MNOPQRSTUVWXYZ != ABC.DEFGHIJKLMNOP
    
  • I change the expectation

    170        self.assertEqual(
    171            src.calculator.subtract(**two_numbers),
    172            self.random_first_number-self.random_second_number
    173        )
    

    the test passes

  • I can use the values method to make a list to test the calculator in test_calculator_w_list_items

    88    def test_calculator_w_list_items(self):
    89        # two_numbers = [self.random_first_number, self.random_second_number]
    90        a_dictionary = {
    91            'x': self.random_first_number,
    92            'y': self.random_second_number
    93        }
    94        two_numbers = list(a_dictionary.values())
    95
    96        self.assertEqual(
    97            src.calculator.add(two_numbers[0], two_numbers[1]),
    98            self.random_first_number+self.random_second_number
    99        )
    

    the test is still green

  • I can also use a dictionary with a for loop to make test_calculator_sends_message_when_input_is_not_a_number more complex and simpler at the same time

    58    def test_calculator_sends_message_when_input_is_not_a_number(self):
    59        error_message = 'brmph?! Numbers only. Try again...'
    60
    61        arithmetic = {
    62            'addition': src.calculator.add,
    63            'subtraction': src.calculator.subtract,
    64            'multiplication': src.calculator.multiply,
    65            'division': src.calculator.divide,
    66        }
    67
    68        for data in (
    69            None,
    70            True, False,
    71            str(), 'text',
    72            tuple(), (0, 1, 2, 'n'),
    73            list(), [0, 1, 2, 'n'],
    74            set(), {0, 1, 2, 'n'},
    75            dict(), {'key': 'value'},
    76        ):
    77            with self.subTest(i=data):
    78                for operation in arithmetic:
    79                    self.assertEqual(
    80                        arithmetic[operation](data, a_random_number()),
    81                        'BOOM!!!'
    82                    )
    83                self.assertEqual(
    84                    src.calculator.add(data, a_random_number()),
    85                    error_message
    86                )
    

    the terminal shows AssertionError for every case in the iterable

    SUBFAILED(i=None) tests/test_calculator.py::TestCalculator::test_calculator_sends_message_when_input_is_not_a_number - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM'
    SUBFAILED(i=True) tests/test_calculator.py::TestCalculator::test_calculator_sends_message_when_input_is_not_a_number - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM'
    SUBFAILED(i=False) tests/test_calculator.py::TestCalculator::test_calculator_sends_message_when_input_is_not_a_number - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM'
    SUBFAILED(i='') tests/test_calculator.py::TestCalculator::test_calculator_sends_message_when_input_is_not_a_number - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM'
    SUBFAILED(i='text') tests/test_calculator.py::TestCalculator::test_calculator_sends_message_when_input_is_not_a_number - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM'
    SUBFAILED(i=()) tests/test_calculator.py::TestCalculator::test_calculator_sends_message_when_input_is_not_a_number - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM'
    SUBFAILED(i=(0, 1, 2, 'n')) tests/test_calculator.py::TestCalculator::test_calculator_sends_message_when_input_is_not_a_number - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM'
    SUBFAILED(i=[]) tests/test_calculator.py::TestCalculator::test_calculator_sends_message_when_input_is_not_a_number - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM'
    SUBFAILED(i=[0, 1, 2, 'n']) tests/test_calculator.py::TestCalculator::test_calculator_sends_message_when_input_is_not_a_number - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM'
    SUBFAILED(i=set()) tests/test_calculator.py::TestCalculator::test_calculator_sends_message_when_input_is_not_a_number - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM'
    SUBFAILED(i={0, 1, 2, 'n'}) tests/test_calculator.py::TestCalculator::test_calculator_sends_message_when_input_is_not_a_number - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM'
    SUBFAILED(i={}) tests/test_calculator.py::TestCalculator::test_calculator_sends_message_when_input_is_not_a_number - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM'
    SUBFAILED(i={'key': 'value'}) tests/test_calculator.py::TestCalculator::test_calculator_sends_message_when_input_is_not_a_number - AssertionError: 'brmph?! Numbers only. Try again...' != 'BOOM'
    

    the test works

    Note

    arithmetic[operation](data, a_random_number()) represents every operation in the arithmetic dictionary and every item in the iterable for example

    arithmetic['addition'](None, a_random_number())
    arithmetic['subtraction']((0, 1, 2, 'n'), a_random_number())
    arithmetic['division'](dict(), a_random_number())
    arithmetic['multiplication']('text', a_random_number())
    

    these 4 examples are translated by the computer to

    src.calculator.add(None, a_random_number())
    src.calculator.subtract((0, 1, 2, 'n'), a_random_number())
    src.calculator.divide(dict(), a_random_number())
    src.calculator.multiply('text', a_random_number())
    

    If I had to write a test for each operation and each data item, I would end up with a total of 52 tests. Too much

  • I change the expectation

    77            with self.subTest(i=data):
    78                for operation in arithmetic:
    79                    self.assertEqual(
    80                        arithmetic[operation](data, a_random_number),
    81                        error_message
    82                    )
    

    the test passes

  • I remove the other assertions

    58    def test_calculator_sends_message_when_input_is_not_a_number(self):
    59        error_message = 'brmph?! Numbers only. Try again...'
    60
    61        arithmetic = {
    62            'addition': src.calculator.add,
    63            'subtraction': src.calculator.subtract,
    64            'multiplication': src.calculator.multiply,
    65            'division': src.calculator.divide,
    66        }
    67
    68        for data in (
    69            None,
    70            True, False,
    71            str(), 'text',
    72            tuple(), (0, 1, 2, 'n'),
    73            list(), [0, 1, 2, 'n'],
    74            set(), {0, 1, 2, 'n'},
    75            dict(), {'key': 'value'},
    76        ):
    77            with self.subTest(i=data):
    78                for operation in arithmetic:
    79                    self.assertEqual(
    80                        arithmetic[operation](data, a_random_number()),
    81                        error_message
    82                    )
    83                self.assertEqual(
    84                    src.calculator.add(data, a_random_number()),
    85                    error_message
    86                )
    87
    88    def test_calculator_w_list_items(self):
    

    this solution is not as easy to read as what was there before especially for someone new to Python. There has to be a better way


test_calculator_functions

I want to use a dictionary to write one test that covers all the 4 calculator functions: addition, subtraction, division and multiplication

RED: make it pass

  • I add a new test

    52        except ZeroDivisionError:
    53            self.assertEqual(
    54                src.calculator.divide(self.random_first_number, 0),
    55                'brmph?! cannot divide by 0. Try again...'
    56            )
    57
    58    def test_calculator_functions(self):
    59        arithmetic = {
    60            'addition': src.calculator.add,
    61            'subtraction': src.calculator.subtract,
    62            'division': src.calculator.divide,
    63            'multiplication': src.calculator.multiply,
    64        }
    65
    66        for operation in arithmetic:
    67            with self.subTest(operation=operation):
    68                self.assertEqual(
    69                    arithmetic[operation](
    70                        self.random_first_number,
    71                        self.random_second_number
    72                    ),
    73                    'BOOM!!!'
    74                )
    75
    76    def test_calculator_sends_message_when_input_is_not_a_number(self):
    

    the terminal shows AssertionError for the 4 arithmetic operations

    SUBFAILED(operation='addition') tests/test_calculator.py::TestCalculator::test_calculator_functions - AssertionError: QRS.TUVWXYZABCDEF != 'BOOM!!!'
    SUBFAILED(operation='subtraction') tests/test_calculator.py::TestCalculator::test_calculator_functions - AssertionError: GHI.JKLMNOPQRSTUVWX != 'BOOM!!!'
    SUBFAILED(operation='division') tests/test_calculator.py::TestCalculator::test_calculator_functions - AssertionError: Y.ABCDEFGHIJKLMNOP != 'BOOM!!!'
    SUBFAILED(operation='multiplication') tests/test_calculator.py::TestCalculator::test_calculator_functions - AssertionError: QRSTUV.WXYZABCDEFG != 'BOOM!!!'
    

GREEN: make it pass

  • I need a way to add the calculations for each operation to the assertion. I add another dictionary

    63            'multiplication': src.calculator.multiply,
    64        }
    65        expectations = {
    66            'addition': (
    67                self.random_first_number+self.random_second_number
    68            ),
    69            'subtraction': (
    70                self.random_first_number-self.random_second_number
    71            ),
    72            'division': (
    73                self.random_first_number/self.random_second_number
    74            ),
    75            'multiplication': (
    76                self.random_first_number*self.random_second_number
    77            )
    78        }
    79
    80        for operation in arithmetic:
    
  • I use the new dictionary for the calculation in the assertion

    80        for operation in arithmetic:
    81            with self.subTest(operation=operation):
    82                self.assertEqual(
    83                    arithmetic[operation](
    84                        self.random_first_number,
    85                        self.random_second_number
    86                    ),
    87                    expectations[operation]
    88                )
    

    the test passes.

This test goes through every operation in the arithmetic dictionary then calls the function that is its value with self.random_first_number and self.random_second_number as input, and checks if the result is the value for the key in the expectations dictionary

  • In other words

    arithmetic['addition'](self.first_random_number, self.second_random_number)
    arithmetic['subtraction'](self.first_random_number, self.second_random_number)
    arithmetic['division'](self.first_random_number, self.second_random_number)
    arithmetic['multiplication'](self.first_random_number, self.second_random_number)
    
  • these four statements get translated by the computer to

    src.calculator.add(self.first_random_number, self.second_random_number)
    src.calculator.subtract(self.first_random_number, self.second_random_number)
    src.calculator.divide((self.first_random_number, self.second_random_number)
    src.calculator.multiply(self.first_random_number, self.second_random_number)
    
  • then the computer checks if the results of the operations are the same as

    expectations['addition']
    expectations['subtraction']
    expectations['division']
    expectations['multiplication']
    

    which are

    self.random_first_number+self.random_second_number
    self.random_first_number-self.random_second_number
    self.random_first_number/self.random_second_number
    self.random_first_number*self.random_second_number
    
  • the test is checking if these statements are equal

    for addition

    src.calculator.add(self.first_random_number, self.second_random_number)
    self.random_first_number+self.random_second_number
    

    for subtraction

    src.calculator.subtract(self.first_random_number, self.second_random_number)
    self.random_first_number-self.random_second_number
    

    for division

    src.calculator.divide((self.first_random_number, self.second_random_number)
    self.random_first_number/self.random_second_number
    

    for multiplication

    src.calculator.multiply(self.first_random_number, self.second_random_number)
    self.random_first_number*self.random_second_number
    

REFACTOR: make it better

  • the two dictionaries in this test have the same keys, I can put them together

     75            'multiplication': (
     76                self.random_first_number*self.random_second_number
     77            )
     78        }
     79
     80        arithmetic_tests = {
     81            'addition': {
     82                'function': src.calculator.add,
     83                'expectation': (
     84                    self.random_first_number+self.random_second_number
     85                ),
     86            },
     87            'subtraction': {
     88                'function': src.calculator.subtract,
     89                'expectation': (
     90                    self.random_first_number-self.random_second_number
     91                ),
     92            },
     93            'division': {
     94                'function': src.calculator.divide,
     95                'expectation': (
     96                    self.random_first_number/self.random_second_number
     97                ),
     98            },
     99            'multiplication': {
    100                'function': src.calculator.divide,
    101                'expectation': (
    102                    self.random_first_number*self.random_second_number
    103                ),
    104            }
    105        }
    106
    107        for operation in arithmetic:
    
  • I add a new assertion in a for loop with the subTest method

    107        for operation in arithmetic:
    108            with self.subTest(operation=operation):
    109                self.assertEqual(
    110                    arithmetic[operation](
    111                        self.random_first_number,
    112                        self.random_second_number
    113                    ),
    114                    expectations[operation]
    115                )
    116
    117        for operation in arithmetic_tests:
    118            with self.subTest(operation=operation):
    119                self.assertEqual(
    120                    arithmetic_tests[operation]['function'](
    121                        self.random_first_number,
    122                        self.random_second_number
    123                    ),
    124                    'BOOM!!!'
    125                )
    126
    127    def test_calculator_sends_message_when_input_is_not_a_number(self):
    

    the terminal shows AssertionError:

    SUBFAILED(operation='addition') tests/test_calculator.py::TestCalculator::test_calculator_functions - AssertionError: HIJ.KLMNOPQRSTUVW != 'BOOM!!!'
    SUBFAILED(operation='subtraction') tests/test_calculator.py::TestCalculator::test_calculator_functions - AssertionError: XYZA.BCDEFGHIJKLMN != 'BOOM!!!'
    SUBFAILED(operation='division') tests/test_calculator.py::TestCalculator::test_calculator_functions - AssertionError: NO.PQRSTUVWXYZABCDE != 'BOOM!!!'
    SUBFAILED(operation='multiplication') tests/test_calculator.py::TestCalculator::test_calculator_functions - AssertionError: FGHIJKLM.NOPQRSTUVW != 'BOOM!!!'
    
  • I change the expectation

    117        for operation in arithmetic_tests:
    118            with self.subTest(operation=operation):
    119                self.assertEqual(
    120                    arithmetic_tests[operation]['function'](
    121                        self.random_first_number,
    122                        self.random_second_number
    123                    ),
    124                    arithmetic_tests[operation]['expectation']
    125                )
    

    the test passes

  • I remove the other dictionaries and for loop

    58    def test_calculator_functions(self):
    59        arithmetic_tests = {
    60            'addition': {
    61                'function': src.calculator.add,
    62                'expectation': (
    63                    self.random_first_number+self.random_second_number
    64                ),
    65            },
    66            'subtraction': {
    67                'function': src.calculator.subtract,
    68                'expectation': (
    69                    self.random_first_number-self.random_second_number
    70                ),
    71            },
    72            'division': {
    73                'function': src.calculator.divide,
    74                'expectation': (
    75                    self.random_first_number/self.random_second_number
    76                ),
    77            },
    78            'multiplication': {
    79                'function': src.calculator.multiply,
    80                'expectation': (
    81                    self.random_first_number*self.random_second_number
    82                ),
    83            }
    84        }
    85
    86        for operation in arithmetic_tests:
    87            with self.subTest(operation=operation):
    88                self.assertEqual(
    89                    arithmetic_tests[operation]['function'](
    90                        self.random_first_number,
    91                        self.random_second_number
    92                    ),
    93                    arithmetic_tests[operation]['expectation']
    94                )
    95
    96    def test_calculator_sends_message_when_input_is_not_a_number(self):
    
  • I remove the test_addition, test_subtraction and test_multiplication methods

    class TestCalculator(unittest.TestCase):
    
        def setUp(self):
            self.random_first_number = a_random_number()
            self.random_second_number = a_random_number()
    
        def test_division(self):
    
  • I need a way to handle ZeroDivisionError in test_calculator_functions for the divide function. I change random_second_number to 0 in the setUp method to make ZeroDivisionError happen in the tests

    12    def setUp(self):
    13        self.random_first_number = a_random_number()
    14        # self.random_second_number = a_random_number()
    15        self.random_second_number = 0
    16
    17    def test_division(self):
    

    the terminal shows ZeroDivisionError for 3 tests

    FAILED tests/test_calculator.py::TestCalculator::test_calculator_functions - ZeroDivisionError: float division by zero
    FAILED tests/test_calculator.py::TestCalculator::test_calculator_w_dictionary_items - ZeroDivisionError: float division by zero
    FAILED tests/test_calculator.py::TestCalculator::test_calculator_w_list_items - ZeroDivisionError: float division by zero
    
  • I use an exception handler to add a new class attribute (variable) to the setUp method for the result of division

    15        self.random_second_number = 0
    16        try:
    17            self.division_result = (
    18                self.random_first_number / self.random_second_number
    19            )
    20        except ZeroDivisionError:
    21            self.division_result = 'brmph?! cannot divide by 0. Try again...'
    
  • I use the new class attribute (variable) in test_calculator_functions

    52            'division': {
    53                'function': src.calculator.divide,
    54                # 'expectation': (
    55                #     self.random_first_number/self.random_second_number
    56                # ),
    57                'expectation': self.division_result,
    58            },
    

    the terminal shows ZeroDivisionError for 2 tests

    FAILED tests/test_calculator.py::TestCalculator::test_calculator_w_dictionary_items - ZeroDivisionError: float division by zero
    FAILED tests/test_calculator.py::TestCalculator::test_calculator_w_list_items - ZeroDivisionError: float division by zero
    

    progress

  • I add the class attribute (variable) to test_calculator_w_list_items

    119        self.assertEqual(
    120            src.calculator.divide(two_numbers[-2], two_numbers[-1]),
    121            # self.random_first_number/self.random_second_number
    122            self.division_result
    123        )
    

    and

    136        self.assertEqual(
    137            src.calculator.divide(*two_numbers),
    138            # self.random_first_number/self.random_second_number
    139            self.division_result
    140        )
    

    the terminal shows ZeroDivisionError for 1 test

    FAILED tests/test_calculator.py::TestCalculator::test_calculator_w_dictionary_items - ZeroDivisionError: float division by zero
    
  • I add the class attribute (variable) to test_calculator_w_dictionary_items

    163        self.assertEqual(
    164            src.calculator.divide(
    165                two_numbers['first_input'],
    166                two_numbers['second_input']
    167            ),
    168            # self.random_first_number/self.random_second_number
    169            self.division_result
    170        )
    

    and

    189        self.assertEqual(
    190            src.calculator.divide(**two_numbers),
    191            # self.random_first_number/self.random_second_number
    192            self.division_result
    193        )
    

    the terminal shows green again. Lovely!

  • I remove the lines I commented out to replace with self.division_result

  • I remove test_division

    12    def setUp(self):
    13        self.random_first_number = a_random_number()
    14        # self.random_second_number = a_random_number()
    15        self.random_second_number = 0
    16        try:
    17            self.division_result = (
    18                self.random_first_number / self.random_second_number
    19            )
    20        except ZeroDivisionError:
    21            self.division_result = 'brmph?! cannot divide by 0. Try again...'
    22
    23    def test_calculator_functions(self):
    
  • I change self.random_second_number back to a random float

    12def setUp(self):
    13    self.random_first_number = a_random_number()
    14    self.random_second_number = a_random_number()
    

    all tests are still green

  • the dictionaries in test_calculator_functions and test_calculator_sends_message_when_input_is_not_a_number are similar, I add a new dictionary in the setUp method to replace them

    19        except ZeroDivisionError:
    20            self.division_result = 'brmph?! cannot divide by 0. Try again...'
    21
    22        self.arithmetic_tests = {
    23            'addition': {
    24                'function': src.calculator.add,
    25                'expectation': (
    26                    self.random_first_number+self.random_second_number
    27                ),
    28            },
    29            'subtraction': {
    30                'function': src.calculator.subtract,
    31                'expectation': (
    32                    self.random_first_number-self.random_second_number
    33                ),
    34            },
    35            'division': {
    36                'function': src.calculator.divide,
    37                'expectation': self.division_result,
    38            },
    39            'multiplication': {
    40                'function': src.calculator.multiply,
    41                'expectation': (
    42                    self.random_first_number*self.random_second_number
    43                ),
    44            }
    45        }
    46
    47    def test_calculator_functions(self):
    
  • then I use it in test_calculator_functions

    73        # for operation in arithmetic_tests:
    74        for operation in self.arithmetic_tests:
    75            with self.subTest(operation=operation):
    76                self.assertEqual(
    77                    # arithmetic_tests[operation]['function'](
    78                    self.arithmetic_tests[operation]['function'](
    79                        self.random_first_number,
    80                        self.random_second_number
    81                    ),
    82                    # arithmetic_tests[operation]['expectation']
    83                    self.arithmetic_tests[operation]['expectation']
    84                )
    

    the test is still green

  • I remove the commented lines and arithemtic_tests dictionary from test_calculator_functions

    47    def test_calculator_functions(self):
    48        for operation in self.arithmetic_tests:
    49            with self.subTest(operation=operation):
    50                self.assertEqual(
    51                    self.arithmetic_tests[operation]['function'](
    52                        self.random_first_number,
    53                        self.random_second_number
    54                    ),
    55                    self.arithmetic_tests[operation]['expectation']
    56                )
    57
    58    def test_calculator_sends_message_when_input_is_not_a_number(self):
    

    still green

  • I use the new class attribute (variable) in test_calculator_sends_message_when_input_is_not_a_number

    77            with self.subTest(i=data):
    78                # for operation in arithmetic:
    79                for operation in self.arithmetic_tests:
    80                    self.assertEqual(
    81                        # arithmetic[operation](data, a_random_number()),
    82                        self.arithmetic_tests[operation]['function'](
    83                            data, a_random_number()
    84                        ),
    85                        error_message
    86                    )
    

    the test is still green

  • I remove the commented lines and the arithmetic dictionary

    58    def test_calculator_sends_message_when_input_is_not_a_number(self):
    59        error_message = 'brmph?! 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):
    71                for operation in self.arithmetic_tests:
    72                    self.assertEqual(
    73                        self.arithmetic_tests[operation]['function'](
    74                            data, a_random_number()
    75                        ),
    76                        error_message
    77                    )
    78
    79    def test_calculator_w_list_items(self):
    

    still green

  • I add a for loop to use the arithmetic_tests dictionary in test_calculator_raises_type_error_when_given_more_than_two_inputs

    180        with self.assertRaises(TypeError):
    181            src.calculator.subtract(*not_two_numbers)
    182
    183        for operation in self.arithmetic_tests:
    184            with self.subTest(operation=operation):
    185                self.arithmetic_tests[operation]['function'](
    186                    **not_two_numbers
    187                )
    188
    189
    190# Exceptions seen
    

    the terminal shows TypeError for all 4 cases

    SUBFAILED(operation='addition') tests/test_calculator.py::TestCalculator::test_calculator_raises_type_error_when_given_more_than_two_inputs - TypeError: only_takes_numbers.<locals>.wrapper() takes 2 positional arguments but 3 were ...
    SUBFAILED(operation='subtraction') tests/test_calculator.py::TestCalculator::test_calculator_raises_type_error_when_given_more_than_two_inputs - TypeError: only_takes_numbers.<locals>.wrapper() takes 2 positional arguments but 3 were ...
    SUBFAILED(operation='division') tests/test_calculator.py::TestCalculator::test_calculator_raises_type_error_when_given_more_than_two_inputs - TypeError: only_takes_numbers.<locals>.wrapper() takes 2 positional arguments but 3 were ...
    SUBFAILED(operation='multiplication') tests/test_calculator.py::TestCalculator::test_calculator_raises_type_error_when_given_more_than_two_inputs - TypeError: only_takes_numbers.<locals>.wrapper() takes 2 positional arguments but 3 were ...
    
  • I add the assertRaises method

    183        for operation in self.arithmetic_tests:
    184            with self.subTest(operation=operation):
    185                with self.assertRaises(TypeError):
    186                    self.arithmetic_tests[operation]['function'](
    187                        *not_two_numbers
    188                    )
    

    the test passes

  • I remove the other assertions

    171    def test_calculator_raises_type_error_when_given_more_than_two_inputs(self):
    172        not_two_numbers = [0, 1, 2]
    173
    174        for operation in self.arithmetic_tests:
    175            with self.subTest(operation=operation):
    176                with self.assertRaises(TypeError):
    177                    self.arithmetic_tests[operation]['function'](
    178                        *not_two_numbers
    179                    )
    180
    181
    182# Exceptions seen
    

    still green

  • I no longer need the variable, I remove it and use the list directly in the assertion

    171    def test_calculator_raises_type_error_when_given_more_than_two_inputs(self):
    172        for operation in self.arithmetic_tests:
    173            with self.subTest(operation=operation):
    174                with self.assertRaises(TypeError):
    175                    self.arithmetic_tests[operation]['function'](
    176                        *[0, 1, 2]
    177                    )
    

    the test is still green

  • Python allows me to put the 2 with statements together as one

    171    def test_calculator_raises_type_error_when_given_more_than_two_inputs(self):
    172        for operation in self.arithmetic_tests:
    173            # with self.subTest(operation=operation):
    174            #     with self.assertRaises(TypeError):
    175            with (
    176                self.subTest(operation=operation),
    177                self.assertRaises(TypeError),
    178            ):
    179                self.arithmetic_tests[operation]['function'](
    180                    *[0, 1, 2]
    181                )
    

    still green

  • I remove the commented lines

    171    def test_calculator_raises_type_error_when_given_more_than_two_inputs(self):
    172        for operation in self.arithmetic_tests:
    173            with (
    174                self.subTest(operation=operation),
    175                self.assertRaises(TypeError),
    176            ):
    177                self.arithmetic_tests[operation]['function'](
    178                    *[0, 1, 2]
    179                )
    
  • I add a for loop to test_calculator_w_list_items

    115        self.assertEqual(
    116            src.calculator.subtract(*two_numbers),
    117            self.random_first_number-self.random_second_number
    118        )
    119
    120        for operation in self.arithmetic_tests:
    121            with self.subTest(operation=operation):
    122                self.assertEqual(
    123                    self.arithmetic_tests[operation]['function'](
    124                        *two_numbers
    125                    ),
    126                    'BOOM!!!'
    127                )
    128
    129    def test_calculator_w_dictionary_items(self):
    

    the terminal shows AssertionError for the 4 operations

    SUBFAILED(operation='addition') tests/test_calculator.py::TestCalculator::test_calculator_w_list_items - AssertionError: YZA.BCDEFGHIJKLMNO != 'BOOM!!!'
    SUBFAILED(operation='subtraction') tests/test_calculator.py::TestCalculator::test_calculator_w_list_items - AssertionError: PQR.STUVWXYZABCDE != 'BOOM!!!'
    SUBFAILED(operation='division') tests/test_calculator.py::TestCalculator::test_calculator_w_list_items - AssertionError: F.GHIJKLMNOPQRSTUV != 'BOOM!!!'
    SUBFAILED(operation='multiplication') tests/test_calculator.py::TestCalculator::test_calculator_w_list_items - AssertionError: WXYABC.DEFGHIJKLMN != 'BOOM!!!'
    
  • I change the expectation

    20        for operation in self.arithmetic_tests:
    21            with self.subTest(operation=operation):
    22                self.assertEqual(
    23                    self.arithmetic_tests[operation]['function'](
    24                        *two_numbers
    25                    ),
    26                    self.arithmetic_tests[operation]['expectation']
    27                )
    

    the test passes

  • I remove all the assertions for the starred expression

     99        self.assertEqual(
    100            src.calculator.subtract(two_numbers[-2], two_numbers[0]),
    101            self.random_first_number-self.random_first_number
    102        )
    103
    104        for operation in self.arithmetic_tests:
    105            with self.subTest(operation=operation):
    106                self.assertEqual(
    107                    self.arithmetic_tests[operation]['function'](
    108                        *two_numbers
    109                    ),
    110                    self.arithmetic_tests[operation]['expectation']
    111                )
    112
    113    def test_calculator_w_dictionary_items(self):
    

    still green

  • I add a for loop to test_calculator_w_dictionary_items

    159        self.assertEqual(
    160            src.calculator.subtract(**two_numbers),
    161            self.random_first_number-self.random_second_number
    162        )
    163
    164        for operation in self.arithmetic_tests:
    165            with self.subTest(operation=operation):
    166                self.assertEqual(
    167                    self.arithmetic_tests[operation]['function'](
    168                        **two_numbers
    169                    ),
    170                    'BOOM!!!'
    171                )
    172
    173    def test_calculator_raises_type_error_when_given_more_than_two_inputs(self):
    

    the terminal shows AssertionError for the 4 operations

    SUBFAILED(operation='addition') tests/test_calculator.py::TestCalculator::test_calculator_w_dictionary_items - AssertionError: OPQ.RSTUVWXYZABCDEF != 'BOOM!!!'
    SUBFAILED(operation='subtraction') tests/test_calculator.py::TestCalculator::test_calculator_w_dictionary_items - AssertionError: GHI.JKLMNOPQRSTUVWX != 'BOOM!!!'
    SUBFAILED(operation='division') tests/test_calculator.py::TestCalculator::test_calculator_w_dictionary_items - AssertionError: Y.ZABCDEFGHIJKLMNOP != 'BOOM!!!'
    SUBFAILED(operation='multiplication') tests/test_calculator.py::TestCalculator::test_calculator_w_dictionary_items - AssertionError: -QRSTU.VWXYZABCDEF != 'BOOM!!!'
    
  • I change the expectation

    164        for operation in self.arithmetic_tests:
    165            with self.subTest(operation=operation):
    166                self.assertEqual(
    167                    self.arithmetic_tests[operation]['function'](
    168                        **two_numbers
    169                    ),
    170                    self.arithmetic_tests[operation]['expectation']
    171                )
    

    the test passes

  • I remove the other assertions for the double starred expression

    140        self.assertEqual(
    141            src.calculator.subtract(
    142                two_numbers['first_input'],
    143                two_numbers['first_input']
    144            ),
    145            self.random_first_number-self.random_first_number
    146        )
    147
    148        for operation in self.arithmetic_tests:
    149            with self.subTest(operation=operation):
    150                self.assertEqual(
    151                    self.arithmetic_tests[operation]['function'](
    152                        **two_numbers
    153                    ),
    154                    self.arithmetic_tests[operation]['expectation']
    155                )
    156
    157    def test_calculator_raises_type_error_when_given_more_than_two_inputs(self):
    

    still green


test squares

Since I am using a dictionary adding a new test is easy. I want to add a test for using the calculator to square numbers like I did in test_making_a_list_w_processes

RED: make it fail

I add a new key for the square function to the arithmetic_tests dictionary

40              'multiplication': {
41                  'function': src.calculator.multiply,
42                  'expectation': (
43                      self.random_first_number*self.random_second_number
44                  ),
45              },
46              'square': {
47                  'function': src.calculator.square,
48                  'expectation': self.random_first_number**2
49              },
50          }
51
52      def test_calculator_functions(self):

the terminal shows AttributeError for every test

AttributeError: module 'src.calculator' has no attribute 'square'

GREEN: make it pass

  • I open calculator.py in the editor then add a new function

    36@only_takes_numbers
    37def add(first_input, second_input):
    38    return first_input + second_input
    39
    40
    41def square(first_input):
    42    return None
    

    the terminal shows TypeError

    TypeError: square() takes 1 positional argument but 2 were given
    
  • I add a second input argument

    41def square(first_input, second_input):
    42    return None
    

    the terminal shows AssertionError for 16 tests

  • I change the return statement

    41def square(first_input, second_input):
    42    return first_input**2
    

    the terminal shows AssertionError for 13 tests. Progress

  • I wrap the square function with the only_takes_numbers decorator function

    41@only_takes_numbers
    42def square(first_input, second_input):
    43    return first_input**2
    

    the test passes

:refactor:`REFACTOR`: make it better

  • I want the square function to only take one input since the second input is not used. I make the second input optional by adding a default argument

    41@only_takes_numbers
    42def square(first_input, second_input=None):
    43    return first_input**2
    

    still green. Not a beautiful solution but it works for now.

    • I use the Rename Symbol feature to change the name of the arithmetic_tests dictionary since I have added a test that is not arithmetic

    20        except ZeroDivisionError:
    21            self.division_result = 'brmph?! cannot divide by 0. Try again...'
    22
    23        self.calculator_tests = {
    24            'addition': {
    25                'function': src.calculator.add,
    26                'expectation': (
    27                    self.random_first_number+self.random_second_number
    28                ),
    29            },
    

    I think it is time to take nap. That was a lot.


close the project

  • I close test_calculator.py in the editor

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

    (.venv) .../pumping_python/calculator
    
  • 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 added the following tests for the calculator program with dictionaries which made testing the program easier


code from the chapter

Do you want to see all the CODE I typed in this chapter?


what is next?

you know

I edited makePythonTdd.sh or makePythonTdd.ps1 for the last few projects. I want to automate the process so that I can call the program and it does all the steps for me when I give it the name of the project.

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


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