test atm with list comprehensions

I want to use for loops in the atm project to give a different message with each denial based on the following inputs

  • has the card expired?

  • is the PIN correct?

  • is there enough cash in the account for the withdrawal?

  • will the withdrawal put the account above or below the daily limit for withdrawals?

the truth table could be

PIN

cash

daily limit

card expired

withdrawal

right PIN

enough cash

above limit

expired

DENIED: Card Expired

right PIN

enough cash

above limit

NOT expired

DENIED: Exceeded the daily withdrawal limit

right PIN

enough cash

NOT above limit

expired

DENIED: Card Expired

right PIN

enough cash

NOT above limit

NOT expired

CASH

PIN

cash

daily limit

card expired

withdrawal

right PIN

NOT enough cash

above limit

expired

DENIED: Card Expired

right PIN

NOT enough cash

above limit

NOT expired

DENIED: Exceeded the daily withdrawal limit

right PIN

NOT enough cash

NOT above limit

expired

DENIED: Card Expired

right PIN

NOT enough cash

NOT above limit

NOT expired

DENIED: Not enough CASH in Account

PIN

cash

daily limit

card expired

withdrawal

wrong PIN

enough cash

above limit

expired

DENIED: Card Expired

wrong PIN

enough cash

above limit

NOT expired

DENIED: Exceeded the daily withdrawal limit

wrong PIN

enough cash

NOT above limit

expired

DENIED: Card Expired

wrong PIN

enough cash

NOT above limit

NOT expired

DENIED: Wrong PIN. Try again…

PIN

cash

daily limit

card expired

withdrawal

wrong PIN

NOT enough cash

above limit

expired

DENIED: Card Expired

wrong PIN

NOT enough cash

above limit

NOT expired

DENIED: Exceeded the daily withdrawal limit

wrong PIN

NOT enough cash

NOT above limit

expired

DENIED: Card Expired

wrong PIN

NOT enough cash

NOT above limit

NOT expired

DENIED: Wrong PIN. Try again…

The ATM only gives me ‘CASH’ when the right PIN is entered, there is enough cash in the account, it is NOT above limit for daily withdrawals, and the bank card has NOT expired.


preview

This is the test I have at the end of the chapter

atm/tests/test_atm.py
 1import src.atm
 2import unittest
 3
 4
 5class TestATM(unittest.TestCase):
 6
 7    def test_atm(self):
 8        booleans = (False, True)
 9        for right_pin in booleans:
10            for enough_cash in booleans:
11                for above_daily_limit in booleans:
12                    for card_expired in booleans:
13                        with self.subTest(
14                            right_pin=right_pin,
15                            enough_cash=enough_cash,
16                            above_daily_limit=above_daily_limit,
17                            card_expired=card_expired,
18                        ):
19                            result = src.atm.withdraw(
20                                right_pin=right_pin,
21                                enough_cash=enough_cash,
22                                above_daily_limit=above_daily_limit,
23                                card_expired=card_expired,
24                            )
25                            if card_expired:
26                                self.assertEqual(
27                                    result,
28                                    'DENIED: Card Expired'
29                                )
30                            elif above_daily_limit:
31                                self.assertEqual(
32                                    result,
33                                    'DENIED: Exceeded daily withdrawal limit'
34                                )
35                            elif not right_pin:
36                                self.assertEqual(
37                                    result,
38                                    'DENIED: Wrong PIN. Try again...'
39                                )
40                            elif not enough_cash:
41                                self.assertEqual(
42                                    result,
43                                    'DENIED: '
44                                    'Not enough CASH in Account'
45                                )
46                            else:
47                                self.assertEqual(
48                                    result,
49                                    'CASH'
50                                )
51
52
53# Exceptions seen
54# AssertionError
55# NameError
56# AttributeError
57# TypeError
58# SyntaxError

open the project


test_if_card_expired

The truth table for if the card has expired is

PIN

cash

daily limit

card expired

withdrawal

right PIN

enough cash

above limit

expired

DENIED: Card Expired

right PIN

enough cash

NOT above limit

expired

DENIED: Card Expired

right PIN

NOT enough cash

above limit

expired

DENIED: Card Expired

right PIN

NOT enough cash

NOT above limit

expired

DENIED: Card Expired

PIN

cash

daily limit

card expired

withdrawal

wrong PIN

enough cash

above limit

expired

DENIED: Card Expired

wrong PIN

enough cash

NOT above limit

expired

DENIED: Card Expired

wrong PIN

NOT enough cash

above limit

expired

DENIED: Card Expired

wrong PIN

NOT enough cash

NOT above limit

expired

DENIED: Card Expired

I can write one function with for loops for these cases.


RED: make it fail


I add a test for if the card has expired

atm/tests/test_atm.py
 8class TestATM(unittest.TestCase):
 9
10    def test_if_card_expired(self):
11        booleans = (True, False)
12        for right_pin in booleans:
13            for enough_cash in booleans:
14                for above_daily_limit in booleans:
15                    with self.subTest(
16                        right_pin=right_pin,
17                        enough_cash=enough_cash,
18                        above_daily_limit=above_daily_limit,
19                    ):
20                        self.assertEqual(
21                            src.atm.withdraw(
22                                right_pin=right_pin,
23                                enough_cash=enough_cash,
24                                above_daily_limit=above_daily_limit,
25                                card_expired=True,
26                            ),
27                            'DENIED: Card Expired'
28                        )
29
30    def test_card_w_below_limit_right_pin_enough_cash(self):

the terminal is my friend, and shows AssertionError

SUBFAILED(right_pin=False, enough_cash=False, above_daily_limit=False) ...
    AssertionError: 'DENIED' != 'DENIED: Card Expired'
SUBFAILED(right_pin=False, enough_cash=False, above_daily_limit=True) ...
    AssertionError: 'DENIED' != 'DENIED: Card Expired'
SUBFAILED(right_pin=False, enough_cash=True, above_daily_limit=False) ...
    AssertionError: 'DENIED' != 'DENIED: Card Expired'
SUBFAILED(right_pin=False, enough_cash=True, above_daily_limit=True) ...
    AssertionError: 'DENIED' != 'DENIED: Card Expired'
SUBFAILED(right_pin=True, enough_cash=False, above_daily_limit=False) ...
    AssertionError: 'DENIED' != 'DENIED: Card Expired'
SUBFAILED(right_pin=True, enough_cash=False, above_daily_limit=True) ...
    AssertionError: 'DENIED' != 'DENIED: Card Expired'
SUBFAILED(right_pin=True, enough_cash=True, above_daily_limit=False) ...
    AssertionError: 'DENIED' != 'DENIED: Card Expired'
SUBFAILED(right_pin=True, enough_cash=True, above_daily_limit=True) ...
    AssertionError: 'DENIED' != 'DENIED: Card Expired'

GREEN: make it pass


  • I open __init__.py from the atm folder in the src folder

  • I change the error message for when card_expired is True in the withdraw function in src/atm/__init__.py

    atm/src/atm/__init__.py
    1def withdraw(
    2        right_pin, enough_cash,
    3        above_daily_limit=False, card_expired=False,
    4    ):
    5    denied = 'DENIED'
    6    if card_expired:
    7        return 'DENIED: Card Expired'
    8    if above_daily_limit:
    

    the terminal is my friend, and shows AssertionError

    FAILED ...test_card_w_above_limit_right_pin_enough_cash -
        AssertionError: 'DENIED: Card Expired' != 'DENIED'
    FAILED ...test_card_w_above_limit_right_pin_not_enough_cash -
        AssertionError: 'DENIED: Card Expired' != 'DENIED'
    FAILED ...test_card_w_above_limit_wrong_pin_enough_cash -
        AssertionError: 'DENIED: Card Expired' != 'DENIED'
    FAILED ...test_card_w_above_limit_wrong_pin_not_enough_cash -
        AssertionError: 'DENIED: Card Expired' != 'DENIED'
    FAILED ...test_card_w_below_limit_right_pin_enough_cash -
        AssertionError: 'DENIED: Card Expired' != 'DENIED'
    FAILED ...test_card_w_below_limit_right_pin_not_enough_cash -
        AssertionError: 'DENIED: Card Expired' != 'DENIED'
    FAILED ...test_card_w_below_limit_wrong_pin_enough_cash -
        AssertionError: 'DENIED: Card Expired' != 'DENIED'
    FAILED ...test_card_w_below_limit_wrong_pin_not_enough_cash -
        AssertionError: 'DENIED: Card Expired' != 'DENIED'
    

    I broke the other tests for if the card has expired with my change.

  • I undo the change

    atm/src/atm/__init__.py
    1def withdraw(
    2        right_pin, enough_cash,
    3        above_daily_limit=False, card_expired=False,
    4    ):
    5    denied = 'DENIED'
    6    if card_expired:
    7        return denied
    8    if above_daily_limit:
    

    the terminal shows the previous AssertionError for test_if_card_expired

  • I add another parameter for card_expired with an if statement so that I do not break the other tests

    atm/src/atm/__init__.py
     1def withdraw(
     2        right_pin, enough_cash,
     3        above_daily_limit=False, card_expired=False,
     4        card_expired_a=False,
     5    ):
     6    denied = 'DENIED'
     7    if card_expired_a:
     8        return 'DENIED: Card Expired'
     9    if card_expired:
    10        return denied
    11    if above_daily_limit:
    
  • I add card_expired_a to the call to the withdraw function from test_if_card_expired in tests/test_atm.py

    atm/tests/test_atm.py
    20                    self.assertEqual(
    21                        src.atm.withdraw(
    22                            right_pin=right_pin,
    23                            enough_cash=enough_cash,
    24                            above_daily_limit=above_daily_limit,
    25                            card_expired=True,
    26                            card_expired_a=True,
    27                        ),
    28                        'DENIED: Card Expired'
    29                    )
    30
    31    def test_card_w_below_limit_right_pin_enough_cash(self):
    

    the test passes.


REFACTOR: make it better


  • I remove the assertion for if the card has expired from test_card_w_below_limit_right_pin_enough_cash because it is now covered by test_if_card_expired

    atm/tests/test_atm.py
    31    def test_card_w_below_limit_right_pin_enough_cash(self):
    32        self.assertEqual(
    33            src.atm.withdraw(
    34                right_pin=True,
    35                enough_cash=True,
    36                above_daily_limit=False,
    37                card_expired=False,
    38            ),
    39            'CASH'
    40        )
    41
    42    def test_card_w_above_limit_right_pin_enough_cash(self):
    
  • I remove the assertion for if the card has expired from test_card_w_above_limit_right_pin_enough_cash because it is now covered by test_if_card_expired

    atm/tests/test_atm.py
    42    def test_card_w_above_limit_right_pin_enough_cash(self):
    43        self.assertEqual(
    44            src.atm.withdraw(
    45                right_pin=True,
    46                enough_cash=True,
    47                above_daily_limit=True,
    48                card_expired=False,
    49            ),
    50            DENIED
    51        )
    52
    53    def test_card_w_above_limit_right_pin_not_enough_cash(self):
    
  • I remove the assertion for if the card has expired from test_card_w_above_limit_right_pin_not_enough_cash because it is now covered by test_if_card_expired

    atm/tests/test_atm.py
    53    def test_card_w_above_limit_right_pin_not_enough_cash(self):
    54        self.assertEqual(
    55            src.atm.withdraw(
    56                right_pin=True,
    57                enough_cash=False,
    58                above_daily_limit=True,
    59                card_expired=False,
    60            ),
    61            DENIED
    62        )
    63
    64    def test_card_w_below_limit_right_pin_not_enough_cash(self):
    
  • I remove the assertion for if the card has expired from test_card_w_below_limit_right_pin_not_enough_cash because it is now covered by test_if_card_expired

    atm/tests/test_atm.py
    64    def test_card_w_below_limit_right_pin_not_enough_cash(self):
    65        self.assertEqual(
    66            src.atm.withdraw(
    67                right_pin=True,
    68                enough_cash=False,
    69                above_daily_limit=False,
    70                card_expired=False,
    71            ),
    72            DENIED
    73        )
    74
    75    def test_card_w_above_limit_wrong_pin_enough_cash(self):
    
  • I remove the assertion for if the card has expired from test_card_w_above_limit_wrong_pin_enough_cash because it is now covered by test_if_card_expired

    atm/tests/test_atm.py
    75    def test_card_w_above_limit_wrong_pin_enough_cash(self):
    76        self.assertEqual(
    77            src.atm.withdraw(
    78                right_pin=False,
    79                enough_cash=True,
    80                above_daily_limit=True,
    81                card_expired=False,
    82            ),
    83            DENIED
    84        )
    85
    86    def test_card_w_below_limit_wrong_pin_enough_cash(self):
    
  • I remove the assertion for if the card has expired from test_card_w_below_limit_wrong_pin_enough_cash because it is now covered by test_if_card_expired

    atm/tests/test_atm.py
    86    def test_card_w_below_limit_wrong_pin_enough_cash(self):
    87        self.assertEqual(
    88            src.atm.withdraw(
    89                right_pin=False,
    90                enough_cash=True,
    91                above_daily_limit=False,
    92                card_expired=False,
    93            ),
    94            DENIED
    95        )
    96
    97    def test_card_w_above_limit_wrong_pin_not_enough_cash(self):
    
  • I remove the assertion for if the card has expired from test_card_w_above_limit_wrong_pin_not_enough_cash because it is now covered by test_if_card_expired

    atm/tests/test_atm.py
     97    def test_card_w_above_limit_wrong_pin_not_enough_cash(self):
     98        self.assertEqual(
     99            src.atm.withdraw(
    100                right_pin=False,
    101                enough_cash=False,
    102                above_daily_limit=True,
    103                card_expired=False,
    104            ),
    105            DENIED
    106        )
    107
    108    def test_card_w_below_limit_wrong_pin_not_enough_cash(self):
    
  • I remove the assertion for if the card has expired from test_card_w_below_limit_wrong_pin_not_enough_cash because it is now covered by test_if_card_expired

    atm/tests/test_atm.py
    108    def test_card_w_below_limit_wrong_pin_not_enough_cash(self):
    109        self.assertEqual(
    110            src.atm.withdraw(
    111                right_pin=False,
    112                enough_cash=False,
    113                above_daily_limit=False,
    114                card_expired=False,
    115            ),
    116            DENIED
    117        )
    118
    119
    120# Exceptions seen
    
  • I remove card_expired_a from the call to the withdraw function from test_if_card_expired in tests/test_atm.py

    atm/tests/test_atm.py
    20                    self.assertEqual(
    21                        src.atm.withdraw(
    22                            right_pin=right_pin,
    23                            enough_cash=enough_cash,
    24                            above_daily_limit=above_daily_limit,
    25                            card_expired=True,
    26                        ),
    27                        'DENIED: Card Expired'
    28                    )
    29
    30    def test_card_w_below_limit_right_pin_enough_cash(self):
    

    the terminal is my friend, and shows AssertionError

  • I change the error message of the if statement for when card_expired is True in the withdraw function in src/atm/__init__.py

    atm/src/atm/__init__.py
     1def withdraw(
     2        right_pin, enough_cash,
     3        above_daily_limit=False, card_expired=False,
     4        card_expired_a=False,
     5    ):
     6    denied = 'DENIED'
     7    if card_expired_a:
     8        return 'DENIED: Card Expired'
     9    if card_expired:
    10        return 'DENIED: Card Expired'
    11    if above_daily_limit:
    

    the test is green again.

  • I remove card_expired_a and its if statement from the withdraw function

    atm/src/atm/__init__.py
    1def withdraw(
    2        right_pin, enough_cash,
    3        above_daily_limit=False, card_expired=False,
    4    ):
    5    denied = 'DENIED'
    6    if card_expired:
    7        return 'DENIED: Card Expired'
    8    if above_daily_limit:
    

    green.

  • I add a git commit message

    git commit -am 'add test_if_card_expired'
    

test_if_above_daily_limit

The truth table for if the account is above limit for daily withdrawals is

PIN

cash

daily limit

card expired

withdrawal

right PIN

enough cash

above limit

NOT expired

DENIED: Exceeded the daily withdrawal limit

right PIN

NOT enough cash

above limit

NOT expired

DENIED: Exceeded the daily withdrawal limit

wrong PIN

enough cash

above limit

NOT expired

DENIED: Exceeded the daily withdrawal limit

wrong PIN

NOT enough cash

above limit

NOT expired

DENIED: Exceeded the daily withdrawal limit

I can write one function with for loops for these cases.


RED: make it fail


I add a test for if the account is above limit for daily withdrawals

atm/tests/test_atm.py
 8class TestATM(unittest.TestCase):
 9
10    def test_if_above_daily_limit(self):
11        booleans = (False, True)
12        for right_pin in booleans:
13            for enough_cash in booleans:
14                with self.subTest(
15                    right_pin=right_pin,
16                    enough_cash=enough_cash,
17                ):
18                    self.assertEqual(
19                        src.atm.withdraw(
20                            right_pin=right_pin,
21                            enough_cash=enough_cash,
22                            above_daily_limit_a=True,
23                            card_expired=False,
24                        ),
25                        'DENIED: exceeded daily withdrawal limit'
26                    )
27
28    def test_if_card_expired(self):

the terminal is my friend, and shows TypeError

  SUBFAILED(right_pin=False, enough_cash=False) ...
      TypeError: withdraw() got an unexpected keyword argument 'enough_Cash'.
                 Did you mean 'above_daily_limit'?
  SUBFAILED(right_pin=False, enough_cash=True) ...
      TypeError: withdraw() got an unexpected keyword argument 'enough_Cash'.
                 Did you mean 'above_daily_limit'?
  SUBFAILED(right_pin=True, enough_cash=False) ...
      TypeError: withdraw() got an unexpected keyword argument 'enough_Cash'.
                 Did you mean 'above_daily_limit'?
  SUBFAILED(right_pin=True, enough_cash=True) ...
      TypeError: withdraw() got an unexpected keyword argument 'enough_Cash'.
                 Did you mean 'above_daily_limit'?

GREEN: make it pass


I add above_daily_limit_a to the withdraw function with an if statement for it, in src/atm/__init__.py

atm/src/atm/__init__.py
 1def withdraw(
 2        right_pin, enough_cash,
 3        above_daily_limit=False, card_expired=False,
 4        above_daily_limit_a=False,
 5    ):
 6    denied = 'DENIED'
 7    if card_expired:
 8        return 'DENIED: Card Expired'
 9    if above_daily_limit_a:
10        return 'DENIED: Exceeded the daily withdrawal limit'
11    if above_daily_limit:

the test passes.


REFACTOR: make it better


  • I remove test_card_w_above_limit_right_pin_enough_cash because it is now covered by test_if_above_daily_limit, in tests/test_atm.py

  • I remove test_card_w_above_limit_right_pin_not_enough_cash because it is now covered by test_if_above_daily_limit

    atm/tests/test_atm.py
    48    def test_card_w_below_limit_right_pin_enough_cash(self):
    49        self.assertEqual(
    50            src.atm.withdraw(
    51                right_pin=True,
    52                enough_cash=True,
    53                above_daily_limit=False,
    54                card_expired=False,
    55            ),
    56            'CASH'
    57        )
    58
    59    def test_card_w_below_limit_right_pin_not_enough_cash(self):
    
  • I remove test_card_w_above_limit_wrong_pin_enough_cash because it is now covered by test_if_above_daily_limit

    atm/tests/test_atm.py
    59    def test_card_w_below_limit_right_pin_not_enough_cash(self):
    60        self.assertEqual(
    61            src.atm.withdraw(
    62                right_pin=True,
    63                enough_cash=False,
    64                above_daily_limit=False,
    65                card_expired=False,
    66            ),
    67            DENIED
    68        )
    69
    70    def test_card_w_below_limit_wrong_pin_enough_cash(self):
    
  • I remove test_card_w_above_limit_wrong_pin_not_enough_cash because it is now covered by test_if_above_daily_limit

    atm/tests/test_atm.py
    70    def test_card_w_below_limit_wrong_pin_enough_cash(self):
    71        self.assertEqual(
    72            src.atm.withdraw(
    73                right_pin=False,
    74                enough_cash=True,
    75                above_daily_limit=False,
    76                card_expired=False,
    77            ),
    78            DENIED
    79        )
    80
    81    def test_card_w_below_limit_wrong_pin_not_enough_cash(self):
    
  • I change above_daily_limit_a to above_daily_limit in the call to the withdraw function from test_if_above_daily_limit in tests/test_atm.py

    atm/tests/test_atm.py
    18                    self.assertEqual(
    19                        src.atm.withdraw(
    20                            right_pin=right_pin,
    21                            enough_cash=enough_cash,
    22                            above_daily_limit=True,
    23                            card_expired=False,
    24                        ),
    25                        'DENIED: Exceeded the daily withdrawal limit'
    26                    )
    27
    28    def test_if_card_expired(self):
    

    the terminal is my friend, and shows AssertionError

    SUBFAILED(right_pin=False, enough_cash=False) ...
        AssertionError: 'DENIED'
                     != 'DENIED: Exceeded daily withdrawal limit'
    SUBFAILED(right_pin=False, enough_cash=True) ...
        AssertionError: 'DENIED'
                     != 'DENIED: Exceeded daily withdrawal limit'
    SUBFAILED(right_pin=True, enough_cash=False) ...
        AssertionError: 'DENIED'
                     != 'DENIED: Exceeded daily withdrawal limit'
    SUBFAILED(right_pin=True, enough_cash=True) ...
        AssertionError: 'DENIED'
                     != 'DENIED: Exceeded daily withdrawal limit'
    
  • I change the error message of the if statement for when above_daily_limit is True in the withdraw function in src/atm/__init__.py

    atm/src/atm/__init__.py
     1def withdraw(
     2        right_pin, enough_cash,
     3        above_daily_limit=False, card_expired=False,
     4        above_daily_limit_a=False,
     5    ):
     6    denied = 'DENIED'
     7    if card_expired:
     8        return 'DENIED: Card Expired'
     9    if above_daily_limit_a:
    10        return 'DENIED: Exceeded the daily withdrawal limit'
    11    if above_daily_limit:
    12        return 'DENIED: Exceeded the daily withdrawal limit'
    13    if not right_pin:
    

    the tests passes.

  • I remove above_daily_limit_a and its if statement from the withdraw function

    atm/src/atm/__init__.py
     1def withdraw(
     2        right_pin, enough_cash,
     3        above_daily_limit=False, card_expired=False,
     4    ):
     5    denied = 'DENIED'
     6    if card_expired:
     7        return 'DENIED: Card Expired'
     8    if above_daily_limit:
     9        return 'DENIED: Exceeded the daily withdrawal limit'
    10    if not right_pin:
    

    the test is still green.

  • I add a git commit message

    git commit -am 'add test_if_above_daily_limit'
    

test_if_wrong_pin

The truth table for if the wrong PIN is entered is

PIN

cash

daily limit

card expired

withdrawal

wrong PIN

enough cash

NOT above limit

NOT expired

DENIED: Wrong PIN. Try again…

wrong PIN

NOT enough cash

NOT above limit

NOT expired

DENIED: Wrong PIN. Try again…

I can write one function with for loops for the two cases.


RED: make it fail


I add a test for if the wrong PIN is entered

atm/tests/test_atm.py
 8class TestATM(unittest.TestCase):
 9
10    def test_if_wrong_pin(self):
11        for enough_cash in (False, True):
12            with self.subTest(enough_cash=enough_cash):
13                self.assertEqual(
14                    src.atm.withdraw(
15                        right_pin=False,
16                        enough_cash=enough_cash,
17                        above_daily_limit=False,
18                        card_expired=False,
19                    ),
20                    'DENIED: Wrong PIN. Try again...'
21                )
22
23    def test_if_above_daily_limit(self):

the terminal is my friend, and shows AssertionError

SUBFAILED(enough_cash=False) AssertionError:
    'DENIED' != 'DENIED: Wrong PIN. Try again...'
SUBFAILED(enough_cash=True) AssertionError:
    'DENIED' != 'DENIED: Wrong PIN. Try again...'

GREEN: make it pass


  • I add right_pin_a to the call to the withdraw function in the assertion

    atm/tests/test_atm.py
    13                self.assertEqual(
    14                    src.atm.withdraw(
    15                        right_pin=False,
    16                        right_pin_a=False,
    17                        enough_cash=enough_cash,
    18                        above_daily_limit=False,
    19                        card_expired=False,
    20                    ),
    21                    'DENIED: Wrong PIN. Try again...'
    22                )
    

    the terminal is my friend, and shows TypeError

    SUBFAILED(enough_cash=False) ...
        TypeError: withdraw() got an unexpected keyword argument 'right_pin_a'.
                   Did you mean 'right_pin'?
    SUBFAILED(enough_cash=True) ...
        TypeError: withdraw() got an unexpected keyword argument 'right_pin_a'.
                   Did you mean 'right_pin'?
    
  • I add right_pin_a with an if statement for it, to the withdraw function in src/atm/__init__.py

    atm/src/atm/__init__.py
     1def withdraw(
     2        right_pin, enough_cash,
     3        above_daily_limit=False, card_expired=False,
     4        right_pin_a=True,
     5    ):
     6    denied = 'DENIED'
     7    if card_expired:
     8        return 'DENIED: Card Expired'
     9    if above_daily_limit:
    10        return 'DENIED: Exceeded the daily withdrawal limit'
    11    if not right_pin_a:
    12        return 'DENIED: Wrong PIN. Try again...'
    13    if not right_pin:
    

    the test passes.


REFACTOR: make it better


  • I remove test_card_w_below_limit_wrong_pin_enough_cash because it is now covered by test_if_wrong_pin, in tests/test_atm.py

  • I remove test_card_w_below_limit_wrong_pin_not_enough_cash because it is now covered by test_if_wrong_pin

    atm/tests/test_atm.py
    73    def test_card_w_below_limit_right_pin_not_enough_cash(self):
    74        self.assertEqual(
    75            src.atm.withdraw(
    76                right_pin=True,
    77                enough_cash=False,
    78                above_daily_limit=False,
    79                card_expired=False,
    80            ),
    81            DENIED
    82        )
    83
    84
    85# Exceptions seen
    
  • I remove right_pin_a from the call to the withdraw function from test_if_wrong_pin

    atm/tests/test_atm.py
    10    def test_if_wrong_pin(self):
    11        for enough_cash in (True, False):
    12            with self.subTest(enough_cash=enough_cash):
    13                self.assertEqual(
    14                    src.atm.withdraw(
    15                        right_pin=False,
    16                        enough_cash=enough_cash,
    17                        above_daily_limit=False,
    18                        card_expired=False,
    19                    ),
    20                    'DENIED: Wrong PIN. Try again...'
    21                )
    22
    23    def test_if_above_daily_limit(self):
    

    the terminal is my friend, and shows ASsertionError

  • I change the error message of the if statement for when right_pin is False in the withdraw function in src/atm/__init__.py

    atm/src/atm/__init__.py
     1def withdraw(
     2        right_pin, enough_cash,
     3        above_daily_limit=False, card_expired=False,
     4        right_pin_a=True,
     5    ):
     6    denied = 'DENIED'
     7    if card_expired:
     8        return 'DENIED: Card Expired'
     9    if above_daily_limit:
    10        return 'DENIED: Exceeded the daily withdrawal limit'
    11    if not right_pin_a:
    12        return 'DENIED: Wrong PIN. Try again...'
    13    if not right_pin:
    14        return 'DENIED: Wrong PIN. Try again...'
    15    if not enough_cash:
    

    the test is green again.

  • I remove right_pin_a and its if statement from the withdraw function in src/atm/__init__.py

    atm/src/atm/__init__.py
     1def withdraw(
     2        right_pin, enough_cash,
     3        above_daily_limit=False, card_expired=False,
     4    ):
     5    denied = 'DENIED'
     6    if card_expired:
     7        return 'DENIED: Card Expired'
     8    if above_daily_limit:
     9        return 'DENIED: Exceeded the daily withdrawal limit'
    10    if not right_pin:
    11        return 'DENIED: Wrong PIN. Try again...'
    12    if not enough_cash:
    13        return denied
    14    return 'CASH'
    

    the test is still green.

  • I add a git commit message

    git commit -am 'add test_if_wrong_pin'
    

test_if_not_enough_cash

There is only one message for if there is NOT enough cash in the account, which is if the right PIN is entered, the card has NOT expired and the account is NOT above limit for daily withdrawals

PIN

cash

daily limit

card expired

withdrawal

right PIN

NOT enough cash

NOT above limit

NOT expired

DENIED: Not enough CASH in Account


RED: make it fail


I change the expectation of the assertion in test_card_w_below_limit_right_pin_not_enough_cash in tests/test_atm.py

atm/tests/test_atm.py
72    def test_card_w_below_limit_right_pin_not_enough_cash(self):
73        self.assertEqual(
74            src.atm.withdraw(
75                right_pin=True,
76                enough_cash=False,
77                above_daily_limit=False,
78                card_expired=False,
79            ),
80            'DENIED: Not enough CASH in Account'
81        )
82
83
84# Exceptions seen

the terminal is my friend, and shows AssertionError

AssertionError:
    'DENIED' != 'DENIED: Not enough CASH in Account

GREEN: make it pass


I change the error message for if there is NOT enough cash in the account, in the withdraw function in src/atm/__init__.py

atm/src/atm/__init__.py
 1def withdraw(
 2        right_pin, enough_cash,
 3        above_daily_limit=False, card_expired=False,
 4    ):
 5    denied = 'DENIED'
 6    if card_expired:
 7        return 'DENIED: Card Expired'
 8    if above_daily_limit:
 9        return 'DENIED: Exceeded the daily withdrawal limit'
10    if not right_pin:
11        return 'DENIED: Wrong PIN. Try again...'
12    if not enough_cash:
13        return 'DENIED: Not enough CASH in Account'
14    return 'CASH'

the test passes.


REFACTOR: make it better


  • I remove the denied variable because it is no longer used

    atm/src/atm/__init__.py
     1def withdraw(
     2        right_pin, enough_cash,
     3        above_daily_limit=False, card_expired=False,
     4    ):
     5    if card_expired:
     6        return 'DENIED: Card Expired'
     7    if above_daily_limit:
     8        return 'DENIED: Exceeded the daily withdrawal limit'
     9    if not right_pin:
    10        return 'DENIED: Wrong PIN. Try again...'
    11    if not enough_cash:
    12        return 'DENIED: Not enough CASH in Account'
    13    return 'CASH'
    
  • I change the name of the test from test_card_w_below_limit_right_pin_not_enough_cash to test_if_not_enough_cash

    atm/tests/test_atm.py
    61    def test_card_w_below_limit_right_pin_enough_cash(self):
    62        self.assertEqual(
    63            src.atm.withdraw(
    64                right_pin=True,
    65                enough_cash=True,
    66                above_daily_limit=False,
    67                card_expired=False,
    68            ),
    69            'CASH'
    70        )
    71
    72    def test_if_not_enough_cash(self):
    73        self.assertEqual(
    74            src.atm.withdraw(
    75                right_pin=True,
    76                enough_cash=False,
    77                above_daily_limit=False,
    78                card_expired=False,
    79            ),
    80            'DENIED: Not enough CASH in Account'
    81        )
    82
    83
    84# Exceptions seen
    
  • I add a git commit message

    git commit -am 'rename to test_if_not_enough_cash'
    

extract test_atm

test_if_card_expired, test_if_above_daily_limit and test_if_wrong_pin all use for loops over a tuple of booleans. I can use a function with for loops to put them together with test_card_w_below_limit_right_pin_enough_cash and test_if_not_enough_cash


RED: make it fail


I add a test for all the cases with for loops

atm/tests/test_atm.py
 8class TestATM(unittest.TestCase):
 9
10    def test_atm(self):
11        booleans = (False, True)
12        for right_pin in booleans:
13            for enough_cash in booleans:
14                for above_daily_limit in booleans:
15                    for card_expired in booleans:
16                        with self.subTest(
17                            right_pin=right_pin,
18                            enough_cash=enough_cash,
19                            above_daily_limit=above_daily_limit,
20                            card_expired=card_expired,
21                        ):
atm/tests/test_atm.py
22                            self.assertEqual(
23                                src.atm.withdraw(
24                                    right_pin=right_pin,
25                                    enough_cash=enough_cash,
26                                    above_daily_limit=above_daily_limit,
27                                    card_expired=card_expired,
28                                ),
29                                'BOOM!!!'
30                            )
31
32    def test_if_wrong_pin(self):

the terminal is my friend, and shows AssertionError

SUBFAILED(
    right_pin=False, enough_cash=False,
    above_daily_limit=False, card_expired=False
) ... AssertionError: 'DENIED: Wrong PIN. Try again...' != 'BOOM!!!'
SUBFAILED(
    right_pin=False, enough_cash=False,
    above_daily_limit=False, card_expired=True
) ... AssertionError: 'DENIED: Card Expired' != 'BOOM!!!'
SUBFAILED(
    right_pin=False, enough_cash=False,
    above_daily_limit=True, card_expired=False
) ... AssertionError: 'DENIED: Exceeded daily withdrawal limit' != 'BOOM!!!'
SUBFAILED(
    right_pin=False, enough_cash=False,
    above_daily_limit=True, card_expired=True
) ... AssertionError: 'DENIED: Card Expired' != 'BOOM!!!'
SUBFAILED(
    right_pin=False, enough_cash=True,
    above_daily_limit=False, card_expired=False
) ... AssertionError: 'DENIED: Wrong PIN. Try again...' != 'BOOM!!!'
SUBFAILED(
    right_pin=False, enough_cash=True,
    above_daily_limit=False, card_expired=True
) ... AssertionError: 'DENIED: Card Expired' != 'BOOM!!!'
SUBFAILED(
    right_pin=False, enough_cash=True,
    above_daily_limit=True, card_expired=False
) ... AssertionError: 'DENIED: Exceeded daily withdrawal limit' != 'BOOM!!!'
SUBFAILED(
    right_pin=False, enough_cash=True,
    above_daily_limit=True, card_expired=True
) ... AssertionError: 'DENIED: Card Expired' != 'BOOM!!!'
SUBFAILED(
    right_pin=True, enough_cash=False,
    above_daily_limit=False, card_expired=False
) ... AssertionError: 'DENIED: Not enough CASH in Account' != 'BOOM!!!'
SUBFAILED(
    right_pin=True, enough_cash=False,
    above_daily_limit=False, card_expired=True
) ... AssertionError: 'DENIED: Card Expired' != 'BOOM!!!'
SUBFAILED(
    right_pin=True, enough_cash=False,
    above_daily_limit=True, card_expired=False
) ... AssertionError: 'DENIED: Exceeded daily withdrawal limit' != 'BOOM!!!'
SUBFAILED(
    right_pin=True, enough_cash=False,
    above_daily_limit=True, card_expired=True
) ... AssertionError: 'DENIED: Card Expired' != 'BOOM!!!'
SUBFAILED(
    right_pin=True, enough_cash=True,
    above_daily_limit=False, card_expired=False
) ... AssertionError: 'CASH' != 'BOOM!!!'
SUBFAILED(
    right_pin=True, enough_cash=True,
    above_daily_limit=False, card_expired=True
) ... AssertionError: 'DENIED: Card Expired' != 'BOOM!!!'
SUBFAILED(
    right_pin=True, enough_cash=True,
    above_daily_limit=True, card_expired=False
) ... AssertionError: 'DENIED: Exceeded daily withdrawal limit' != 'BOOM!!!'
SUBFAILED(
    right_pin=True, enough_cash=True,
    above_daily_limit=True, card_expired=True
) ... AssertionError: 'DENIED: Card Expired' != 'BOOM!!!'

GREEN: make it pass


  • I add a variable for the result of the call to the withdraw function then an if statement for if the card has expired

    atm/tests/test_atm.py
    16                        with self.subTest(
    17                            right_pin=right_pin,
    18                            enough_cash=enough_cash,
    19                            above_daily_limit=above_daily_limit,
    20                            card_expired=card_expired,
    21                        ):
    22                            result = src.atm.withdraw(
    23                                right_pin=right_pin,
    24                                enough_cash=enough_cash,
    25                                above_daily_limit=above_daily_limit,
    26                                card_expired=card_expired,
    27                            )
    28                            if card_expired:
    29                                self.assertEqual(
    30                                    result,
    31                                    'DENIED: Card Expired'
    32                                )
    33                            else:
    34                                self.assertEqual(
    35                                    result,
    36                                    'BOOM!!!'
    37                                )
    38
    39    def test_if_wrong_pin(self):
    

    the terminal shows AssertionError for eight out of the sixteen cases. Progress!

  • I add an if statement for if the account is above limit for withdrawals

    atm/tests/test_atm.py
    28                            if card_expired:
    29                                self.assertEqual(
    30                                    result,
    31                                    'DENIED: Card Expired'
    32                                )
    33                            else:
    34                                if above_daily_limit:
    35                                    self.assertEqual(
    36                                        result,
    37                                        'DENIED: '
    38                                        'Exceeded daily withdrawal limit'
    39                                    )
    40                                else:
    41                                    self.assertEqual(
    42                                        result,
    43                                        'BOOM!!!'
    44                                    )
    45
    46    def test_if_wrong_pin(self):
    

    the terminal shows AssertionError for four out of the sixteen cases. More progress, and my if statements are getting more nested. Ugly!

  • I add an if statement for if the wrong PIN is entered

    atm/tests/test_atm.py
    39                                if above_daily_limit:
    40                                    self.assertEqual(
    41                                        result,
    42                                        'DENIED: '
    43                                        'Exceeded daily withdrawal limit'
    44                                    )
    45                                else:
    46                                    if not right_pin:
    47                                        self.assertEqual(
    48                                            result,
    49                                            'DENIED: Wrong PIN.'
    50                                            ' Try again...'
    51                                        )
    52                                    else:
    53                                        self.assertEqual(
    54                                            result,
    55                                            'BOOM!!!'
    56                                        )
    57
    58    def test_if_wrong_pin(self):
    

    the terminal shows AssertionError for two out of the sixteen cases, and the nesting is uglier than before. Oh boy!

  • I add an if statement for if there is NOT enough cash in the account

    atm/tests/test_atm.py
    40                                    if not right_pin:
    41                                        self.assertEqual(
    42                                            result,
    43                                            'DENIED: Wrong PIN. Try again...'
    44                                        )
    45                                    else:
    46                                        if not enough_cash:
    47                                            self.assertEqual(
    48                                                result,
    49                                                'DENIED: '
    50                                                'Not enough CASH in Account'
    51                                            )
    52                                        else:
    53                                            self.assertEqual(
    54                                                result,
    55                                                'BOOM!!!'
    56                                            )
    57
    58    def test_if_wrong_pin(self):
    

    the terminal shows AssertionError for the only cash where I get ‘CASH’

    SUBFAILED(
        right_pin=True, enough_cash=True,
        above_daily_limit=False, card_expired=False
    ) ...AssertionError: 'CASH' != 'BOOM!!!'
    

    more ugly nesting.

  • I change the expectation of the assertion to match

    atm/tests/test_atm.py
    45                                    else:
    46                                        if not enough_cash:
    47                                            self.assertEqual(
    48                                                result,
    49                                                'DENIED: '
    50                                                'Not enough CASH in Account'
    51                                            )
    52                                        else:
    53                                            self.assertEqual(
    54                                                result,
    55                                                'CASH'
    56                                            )
    57
    58    def test_if_wrong_pin(self):
    

    the test passes.


the elif statement

I can write nested if statements with the elif statement

if something:
    do this
else:
    if something_else:
        do that
    else:
        do more

becomes

if something:
    do this
elif something_else:
    do that
else:
    do more

REFACTOR: make it better


  • I change the nested if statements to elif statements

    atm/tests/test_atm.py
    28                            if card_expired:
    29                                self.assertEqual(
    30                                    result,
    31                                    'DENIED: Card Expired'
    32                                )
    33                            elif above_daily_limit:
    34                                self.assertEqual(
    35                                    result,
    36                                    'DENIED: Exceeded daily withdrawal limit'
    37                                )
    
    atm/tests/test_atm.py
    38                            elif not right_pin:
    39                                self.assertEqual(
    40                                    result,
    41                                    'DENIED: Wrong PIN. Try again...'
    42                                )
    43                            elif not enough_cash:
    44                                self.assertEqual(
    45                                    result,
    46                                    'DENIED: '
    47                                    'Not enough CASH in Account'
    48                                )
    49                            else:
    50                                self.assertEqual(
    51                                    result,
    52                                    'CASH'
    53                                )
    54
    55    def test_if_wrong_pin(self):
    

    the test is still green.

  • I remove test_if_wrong_pin because it is now covered by test_atm

  • I remove test_if_above_daily_limit because it is now covered by test_atm

  • I remove test_if_card_expired because it is now covered by test_atm

  • I remove test_card_w_below_limit_right_pin_enough_cash because it is now covered by test_atm

  • I remove test_if_not_enough_cash because it is now covered by test_atm

    atm/tests/test_atm.py
    49                            else:
    50                                self.assertEqual(
    51                                    result,
    52                                    'CASH'
    53                                )
    54
    55
    56# Exceptions seen
    
  • I remove the DENIED because it is no longer used

    atm/tests/test_atm.py
    1import src.atm
    2import unittest
    3
    4
    5class TestATM(unittest.TestCase):
    
  • I add a git commit message

    git commit -am 'add test_if_not_enough_cash'
    

close the project

  • I close tests/test_atm.py and src/atm/__init__.py

  • I click in the terminal where the tests are running

  • I use q on the keyboard to leave the tests. The terminal shows

    .../pumping_python
    

    I am back in the pumping_python directory.


review

I ran tests for an Automated Teller Machine with these inputs:

  • has the card expired?

  • is the PIN correct?

  • is there enough cash in the account for the withdrawal?

  • will the withdrawal put the account above or below the daily limit for withdrawals?

the inputs gave me this truth table with a message for each type of denial, so that the user knows why the withdrawal failed

PIN

cash

daily limit

card expired

withdrawal

right PIN

enough cash

above limit

expired

DENIED: Card Expired

right PIN

enough cash

NOT above limit

expired

DENIED: Card Expired

right PIN

NOT enough cash

above limit

expired

DENIED: Card Expired

right PIN

NOT enough cash

NOT above limit

expired

DENIED: Card Expired

PIN

cash

daily limit

card expired

withdrawal

wrong PIN

enough cash

above limit

expired

DENIED: Card Expired

wrong PIN

enough cash

NOT above limit

expired

DENIED: Card Expired

wrong PIN

NOT enough cash

above limit

expired

DENIED: Card Expired

wrong PIN

NOT enough cash

NOT above limit

expired

DENIED: Card Expired

PIN

cash

daily limit

card expired

withdrawal

right PIN

enough cash

above limit

NOT expired

DENIED: Exceeded the daily withdrawal limit

right PIN

NOT enough cash

above limit

NOT expired

DENIED: Exceeded the daily withdrawal limit

wrong PIN

enough cash

above limit

NOT expired

DENIED: Exceeded the daily withdrawal limit

wrong PIN

NOT enough cash

above limit

NOT expired

DENIED: Exceeded the daily withdrawal limit

PIN

cash

daily limit

card expired

withdrawal

wrong PIN

enough cash

NOT above limit

NOT expired

DENIED: Wrong PIN. Try again…

wrong PIN

NOT enough cash

NOT above limit

NOT expired

DENIED: Wrong PIN. Try again…

PIN

cash

daily limit

card expired

withdrawal

right PIN

NOT enough cash

NOT above limit

NOT expired

DENIED: Not enough CASH in Account

right PIN

enough cash

NOT above limit

NOT expired

CASH

  • I used four nested loops to put all the tests together which makes for one test but ugly nesting.

  • The for loops led to significantly less lines of code.

  • It is also now harder to understand what each thing tests, compared to when I had one assertion for each case, there has to be a better way.


code from the chapter

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


what is next?

Would you like to test booleans with loops?


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.