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
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
I open a terminal
I change directory to the atm folder in the
pumping_pythonfoldercd atmI open
test_atm.pyfrom thetestsfolderI use pytest-watcher to run the tests automatically
uv run pytest-watcher . --now
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
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 change the error message for when
card_expiredis True in thewithdrawfunction insrc/atm/__init__.py1def 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
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_expiredwith an if statement so that I do not break the other testsI add
card_expired_ato the call to thewithdrawfunction from test_if_card_expired intests/test_atm.py20 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
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
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
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
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
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
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
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
I remove
card_expired_afrom the call to thewithdrawfunction from test_if_card_expired intests/test_atm.py20 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_expiredis True in thewithdrawfunction insrc/atm/__init__.py1def 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_aand its if statement from thewithdrawfunction1def 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
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
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.pyI remove test_card_w_above_limit_right_pin_not_enough_cash because it is now covered by test_if_above_daily_limit
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
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
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_atoabove_daily_limitin the call to thewithdrawfunction from test_if_above_daily_limit intests/test_atm.py18 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_limitis True in thewithdrawfunction insrc/atm/__init__.py1def 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_aand its if statement from thewithdrawfunction1def 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
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_ato the call to thewithdrawfunction in the assertion13 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_awith an if statement for it, to thewithdrawfunction insrc/atm/__init__.py1def 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.pyI remove test_card_w_below_limit_wrong_pin_not_enough_cash because it is now covered by test_if_wrong_pin
I remove
right_pin_afrom the call to thewithdrawfunction from test_if_wrong_pin10 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_pinis False in thewithdrawfunction insrc/atm/__init__.py1def 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_aand its if statement from thewithdrawfunction insrc/atm/__init__.py1def 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
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
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
deniedvariable because it is no longer used1def 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
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 seenI 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
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 ):
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
withdrawfunction then an if statement for if the card has expired16 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
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
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
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
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
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 )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
I remove the DENIED because it is no longer used
I add a git commit message
git commit -am 'add test_if_not_enough_cash'
close the project
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 |
code from the chapter
what is next?
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.