Created
September 29, 2021 18:34
-
-
Save ProbonoBonobo/a424fc5e7733cec5865c17918b976a6a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
import re | |
from unittest import TestCase | |
import unittest | |
import subprocess | |
from subprocess import PIPE | |
import time | |
import json | |
class Session: | |
def __init__(self, username='kevin', pin='3141', noauth=False): | |
self.username = username | |
self.pin = pin | |
self.proc = subprocess.Popen("python app.py", | |
shell=True, | |
stdout=PIPE, | |
stdin=PIPE, | |
stderr=PIPE) | |
self._buffer = [] | |
self._stdout = None | |
self._stderr = None | |
if not noauth: | |
self.authenticate(username, pin) | |
def authenticate(self, username, pin): | |
self.write(username, pin, username, pin) | |
def write(self, *lines): | |
for line in lines: | |
self.proc.stdin.write(str(line).encode() + b'\n') | |
self._buffer.append(str(line)) | |
time.sleep(0.1) | |
def __repr__(self): | |
return json.dumps({"stdin": self._buffer, | |
"stdout": self.stdout.split("\n"), | |
"stderr": self.stderr.split("\n"), | |
"ok": not self.stderr.split("\n")}, indent=4) | |
@property | |
def stdout(self): | |
if self._stdout: | |
return self._stdout.decode() | |
try: | |
self._stdout, self._stderr = self.proc.communicate(timeout=1) | |
except: | |
pass | |
return self._stdout.decode() | |
@property | |
def stderr(self): | |
if self._stderr: | |
return self._stderr.decode() | |
try: | |
self._stdout, self._stderr = self.proc.communicate(timeout=1) | |
except: | |
pass | |
return self._stderr.decode() | |
class TestATM(TestCase): | |
def test_submission_implements_correct_directory_structure(self): | |
import sys, os | |
os.chdir(os.path.dirname(__file__)) | |
assert os.path.isfile("app.py") | |
assert os.path.isdir("banking_pkg") | |
assert os.path.isfile("banking_pkg/account.py") | |
try: | |
from banking_pkg import account | |
assert hasattr(account, 'show_balance') | |
assert hasattr(account, 'withdraw') | |
assert hasattr(account, 'deposit') | |
assert hasattr(account, 'logout') | |
except ImportError: | |
raise AssertionError(f'Failed to import app from {os.getcwd()}') | |
def test_interface_rejects_registration_attempt_with_invalid_name(self): | |
sess = Session(username='Mr. Guido van Rossum', pin='3141') | |
sess.write(4) | |
assert 'registered' not in sess.stdout.lower(), "Names longer than 10 characters should be rejected" | |
def test_interface_rejects_registration_attempt_with_invalid_pin(self): | |
sess = Session(username='kevin', pin='asdf') | |
sess.write(4) | |
assert 'registered' not in sess.stdout.lower(), """ | |
I had to double-check the instructions on this one: surprisingly neither Task 2 | |
nor Bonus Task 2 explicitly say to reject non-numeric inputs, only that "a PIN | |
should consist of 4 numbers." (Although it also says "4 characters" in other places, | |
so... ¯\_(ツ)_/¯ ?) Thank goodness, because that means your points are safe. Note | |
that string objects in Python have a built-in method, `isnumeric`, which works much | |
like other string methods we've seen, such as `upper` and `strip`: | |
>>> "32".isnumeric() | |
True | |
>>> "3.14159265358979".isnumeric() | |
False | |
As you can see it returns a True/False value corresponding simply to whether all character | |
points contained in a string correspond to decimal (0-9) numbers. Definitely a convenient | |
utility function for this exercise. (See also: `isalpha` and `isalnum` which work in much | |
the same way.) | |
""" | |
def test_interface_rejects_authentication_attempt_with_invalid_pin(self): | |
sess = Session(noauth=True) | |
sess.write("kevin", "1234", "kevin", "9876", 4) | |
assert """| 1. Balance | 2. Deposit |""" not in sess.stdout | |
def test_atm_implements_customer_show_balance_functionality(self): | |
sess = Session() | |
sess.write(1) | |
assert re.search(r"balance[^\d]+?0[^\d]", | |
sess.stdout.lower()), "Balance should be $100" | |
def test_atm_implements_customer_deposit_functionality(self): | |
sess = Session() | |
sess.write(2, 100, 1) | |
assert re.search(r"balance[^\d]+?100[^\d]", | |
sess.stdout.lower()), "Balance should be $100" | |
def test_atm_implements_customer_withdraw_functionality(self): | |
sess = Session() | |
sess.write('2', 100, '3', 10, '1') | |
assert re.search(r"balance[^\d]+?90", | |
sess.stdout.lower()), "Balance should be $90" | |
def test_atm_implements_customer_logout_functionality(self): | |
sess = Session(username='kevin') | |
sess.write(4) | |
assert not sess.stderr or 'bye' in sess.stdout | |
def test_atm_says_goodbye_when_customer_logs_out(self): | |
sess = Session(username='kevin') | |
sess.write(4) | |
assert re.search(r"bye.{,3}?kevin", | |
sess.stdout.lower()), "A few of you have a working logout feature, but forgot to print 'Goodbye, " \ | |
"{name}!' before terminating the application. I'm not deducting any points for this " \ | |
"but it's a good habit to double check your submissions one last time before turning them " \ | |
"in to make sure they conform to spec! It seems kinda nitpicky, but certain people I " \ | |
"have worked for over the years have been *extremely* rigid about not accepting deliverables " \ | |
"that deviate from the spec even in tiny ways. (I hope I never turn into that kind of person)" | |
def test_interface_rejects_withdrawal_amounts_greater_than_account_balance(self): | |
sess = Session() | |
sess.write('3', '100', '1') | |
assert re.search(r"balance[^\d]+?0[^\d]", sess.stdout.lower()), 'Hmmmm is account balance non-zero?' | |
def test_interface_prevents_negative_withdrawals(self): | |
sess = Session() | |
sess.write('3', '-1000000000', '1') | |
assert not re.search(r"balance[^\d]+?1000000000", | |
sess.stdout.lower()), "An easy-to-miss, but noteworthy/hilarious edge case: note what " \ | |
"happens when a customer withdraws a negative amount. If our ATM " \ | |
"doesn't explicitly prohibit this, that customer's balance will then " \ | |
"increment by the absolute value of the deposit. Feel free to... not " \ | |
"fix this. Definitely a feature. (Woooooooooooooooo! Just made a " \ | |
"billion dollars QA testing! Best ATM ever.)" | |
def test_interface_prints_nicely_formatted_USD_currency_amounts(self): | |
sess = Session() | |
sess.write('3.14159265358979323', '1') | |
assert re.search( | |
r"\$?\s*3.14\b", | |
sess.stdout), """Totally optional, but Python's defines a handy built-in function, `round`, which can be useful | |
when working with floating point values -- especially currencies. It takes a floating point number, an integer-valued | |
number of decimal places, and rounds the floating point number to the specified precision. In the case of an ATM | |
interface, it might make sense to ensure that fractional account balances/deposits/withdrawals are rounded to the | |
nearest currency unit (e.g., 2 units of decimal precision): | |
>>> round(3.14159265358979, 2) | |
3.14 | |
""" | |
import sys | |
def main(out=sys.stderr, verbosity=4): | |
loader = unittest.TestLoader() | |
suite = loader.loadTestsFromModule(sys.modules[__name__]) | |
unittest.TextTestRunner(out, verbosity=verbosity).run(suite) | |
if __name__ == '__main__': | |
with open('unittest_results.txt', 'w') as f: | |
f.write(f"\nRunning tests for {os.path.dirname(__file__)}/app.py...\n\n") | |
main(f) | |
""" | |
Sample output: | |
cat output | |
$ python -m unittest --verbose --locals | |
test_atm_implements_customer_deposit_functionality (test_app.TestATM) ... ok | |
test_atm_implements_customer_logout_functionality (test_app.TestATM) ... ok | |
test_atm_implements_customer_show_balance_functionality (test_app.TestATM) ... ok | |
test_atm_implements_customer_withdraw_functionality (test_app.TestATM) ... ok | |
test_atm_says_goodbye_when_customer_logs_out (test_app.TestATM) ... ok | |
test_interface_prevents_negative_withdrawals (test_app.TestATM) ... FAIL | |
test_interface_prints_nicely_formatted_USD_currency_amounts (test_app.TestATM) ... FAIL | |
test_interface_rejects_authentication_attempt_with_invalid_pin (test_app.TestATM) ... ok | |
test_interface_rejects_registration_attempt_with_invalid_name (test_app.TestATM) ... FAIL | |
test_interface_rejects_registration_attempt_with_invalid_pin (test_app.TestATM) ... FAIL | |
test_interface_rejects_withdrawal_amounts_greater_than_account_balance (test_app.TestATM) ... ok | |
test_submission_implements_correct_directory_structure (test_app.TestATM) ... ok | |
====================================================================== | |
FAIL: test_interface_prevents_negative_withdrawals (test_app.TestATM) | |
---------------------------------------------------------------------- | |
Traceback (most recent call last): | |
File "/home/kz/projects/nucamp/week1/Python Fundamentals-Week 2 Workshop Assignment-ONPA-2021-08-07-4109/student_92676_assignsubmission_file_/workshop2/test_app.py", line 156, in test_interface_prevents_negative_withdrawals | |
assert not re.search(r"balance[^\d]+?1000000000", | |
self = <test_app.TestATM testMethod=test_interface_prevents_negative_withdrawals> | |
sess = { | |
"stdin": [ | |
"kevin", | |
"3141", | |
"kevin", | |
"3141", | |
"3", | |
"-1000000000", | |
"1" | |
], | |
"stdout": [ | |
"Please register your name> Enter your pin> kevin has been registered with a initial balance of $0.0", | |
" === Automated Teller Machine === ", | |
"LOGIN", | |
"Enter name> Enter pin> Login successful!", | |
"", | |
" === Automated Teller Machine === ", | |
"User: kevin", | |
"------------------------------------------", | |
"| 1. Balance | 2. Deposit |", | |
"------------------------------------------", | |
"------------------------------------------", | |
"| 3. Withdraw | 4. Logout |", | |
"------------------------------------------", | |
"Choose an option> Enter amount to withdraw> Your new balance is: $1000000000.0", | |
"", | |
" === Automated Teller Machine === ", | |
"User: kevin", | |
"------------------------------------------", | |
"| 1. Balance | 2. Deposit |", | |
"------------------------------------------", | |
"------------------------------------------", | |
"| 3. Withdraw | 4. Logout |", | |
"------------------------------------------", | |
"Choose an option> 1000000000.0", | |
"", | |
" === Automated Teller Machine === ", | |
"User: kevin", | |
"------------------------------------------", | |
"| 1. Balance | 2. Deposit |", | |
"------------------------------------------", | |
"------------------------------------------", | |
"| 3. Withdraw | 4. Logout |", | |
"------------------------------------------", | |
"Choose an option> " | |
], | |
"stderr": [ | |
"Traceback (most recent call last):", | |
" File \"/home/kz/projects/nucamp/week1/Python Fundamentals-Week 2 Workshop Assignment-ONPA-2021-08-07-4109/student_92676_assignsubmission_file_/workshop2/app.py\", line 32, in <module>", | |
" option = input(\"Choose an option> \")", | |
"EOFError: EOF when reading a line", | |
"" | |
], | |
"ok": false | |
} | |
AssertionError: An easy-to-miss, but noteworthy/hilarious edge case: note what happens when a customer withdraws a negative amount. If our ATM doesn't explicitly prohibit this, that customer's balance will then increment by the absolute value of the deposit. Feel free to... not fix this. Definitely a feature. (Woooooooooooooooo! Just made a billion dollars QA testing! Best ATM ever.) | |
====================================================================== | |
FAIL: test_interface_prints_nicely_formatted_USD_currency_amounts (test_app.TestATM) | |
---------------------------------------------------------------------- | |
Traceback (most recent call last): | |
File "/home/kz/projects/nucamp/week1/Python Fundamentals-Week 2 Workshop Assignment-ONPA-2021-08-07-4109/student_92676_assignsubmission_file_/workshop2/test_app.py", line 167, in test_interface_prints_nicely_formatted_USD_currency_amounts | |
assert re.search( | |
self = <test_app.TestATM testMethod=test_interface_prints_nicely_formatted_USD_currency_amounts> | |
sess = { | |
"stdin": [ | |
"kevin", | |
"3141", | |
"kevin", | |
"3141", | |
"3.14159265358979323", | |
"1" | |
], | |
"stdout": [ | |
"Please register your name> Enter your pin> kevin has been registered with a initial balance of $0.0", | |
" === Automated Teller Machine === ", | |
"LOGIN", | |
"Enter name> Enter pin> Login successful!", | |
"", | |
" === Automated Teller Machine === ", | |
"User: kevin", | |
"------------------------------------------", | |
"| 1. Balance | 2. Deposit |", | |
"------------------------------------------", | |
"------------------------------------------", | |
"| 3. Withdraw | 4. Logout |", | |
"------------------------------------------", | |
"Choose an option> Invalid option. Try again", | |
"", | |
" === Automated Teller Machine === ", | |
"User: kevin", | |
"------------------------------------------", | |
"| 1. Balance | 2. Deposit |", | |
"------------------------------------------", | |
"------------------------------------------", | |
"| 3. Withdraw | 4. Logout |", | |
"------------------------------------------", | |
"Choose an option> 0.0", | |
"", | |
" === Automated Teller Machine === ", | |
"User: kevin", | |
"------------------------------------------", | |
"| 1. Balance | 2. Deposit |", | |
"------------------------------------------", | |
"------------------------------------------", | |
"| 3. Withdraw | 4. Logout |", | |
"------------------------------------------", | |
"Choose an option> " | |
], | |
"stderr": [ | |
"Traceback (most recent call last):", | |
" File \"/home/kz/projects/nucamp/week1/Python Fundamentals-Week 2 Workshop Assignment-ONPA-2021-08-07-4109/student_92676_assignsubmission_file_/workshop2/app.py\", line 32, in <module>", | |
" option = input(\"Choose an option> \")", | |
"EOFError: EOF when reading a line", | |
"" | |
], | |
"ok": false | |
} | |
AssertionError: Totally optional, but Python's defines a handy built-in function, `round`, which can be useful | |
when working with floating point values -- especially currencies. It takes a floating point number, an integer-valued | |
number of decimal places, and rounds the floating point number to the specified precision. In the case of an ATM | |
interface, it might make sense to ensure that fractional account balances/deposits/withdrawals are rounded to the | |
nearest currency unit (e.g., 2 units of decimal precision): | |
>>> round(3.14159265358979, 2) | |
3.14 | |
====================================================================== | |
FAIL: test_interface_rejects_registration_attempt_with_invalid_name (test_app.TestATM) | |
---------------------------------------------------------------------- | |
Traceback (most recent call last): | |
File "/home/kz/projects/nucamp/week1/Python Fundamentals-Week 2 Workshop Assignment-ONPA-2021-08-07-4109/student_92676_assignsubmission_file_/workshop2/test_app.py", line 84, in test_interface_rejects_registration_attempt_with_invalid_name | |
assert 'registered' not in sess.stdout.lower(), "Names longer than 10 characters should be rejected" | |
self = <test_app.TestATM testMethod=test_interface_rejects_registration_attempt_with_invalid_name> | |
sess = { | |
"stdin": [ | |
"Mr. Guido van Rossum", | |
"3141", | |
"Mr. Guido van Rossum", | |
"3141", | |
"4" | |
], | |
"stdout": / | |
"Please register your name> Enter your pin> Mr. Guido van Rossum has been registered with a initial balance of $0.0", | |
" === Automated Teller Machine === ", | |
"LOGIN", | |
"Enter name> Enter pin> Login successful!", | |
"", | |
" === Automated Teller Machine === ", | |
"User: Mr. Guido van Rossum", | |
"------------------------------------------", | |
"| 1. Balance | 2. Deposit |", | |
"------------------------------------------", | |
"------------------------------------------", | |
"| 3. Withdraw | 4. Logout |", | |
"------------------------------------------", | |
"Choose an option> Goodbye Mr. Guido van Rossum.", | |
"" | |
], | |
"stderr": [ | |
"" | |
], | |
"ok": false | |
} | |
AssertionError: Names longer than 10 characters should be rejected | |
====================================================================== | |
FAIL: test_interface_rejects_registration_attempt_with_invalid_pin (test_app.TestATM) | |
---------------------------------------------------------------------- | |
Traceback (most recent call last): | |
File "/home/kz/projects/nucamp/week1/Python Fundamentals-Week 2 Workshop Assignment-ONPA-2021-08-07-4109/student_92676_assignsubmission_file_/workshop2/test_app.py", line 89, in test_interface_rejects_registration_attempt_with_invalid_pin | |
assert 'registered' not in sess.stdout.lower(), """ | |
self = <test_app.TestATM testMethod=test_interface_rejects_registration_attempt_with_invalid_pin> | |
sess = { | |
"stdin": [ | |
"kevin", | |
"asdf", | |
"kevin", | |
"asdf", | |
"4" | |
], | |
"stdout": [ | |
"Please register your name> Enter your pin> kevin has been registered with a initial balance of $0.0", | |
" === Automated Teller Machine === ", | |
"LOGIN", | |
"Enter name> Enter pin> Login successful!", | |
"", | |
" === Automated Teller Machine === ", | |
"User: kevin", | |
"------------------------------------------", | |
"| 1. Balance | 2. Deposit |", | |
"------------------------------------------", | |
"------------------------------------------", | |
"| 3. Withdraw | 4. Logout |", | |
"------------------------------------------", | |
"Choose an option> Goodbye kevin.", | |
"" | |
], | |
"stderr": [ | |
"" | |
], | |
"ok": false | |
} | |
AssertionError: | |
I had to double-check the instructions on this one: surprisingly neither Task 2 | |
nor Bonus Task 2 explicitly say to reject non-numeric inputs, only that "a PIN | |
should consist of 4 numbers." (Although it also says "4 characters" in other places, | |
so... ¯\_(ツ)_/¯ ?) Thank goodness, because that means your points are safe. Note | |
that string objects in Python have a built-in method, `isnumeric`, which works much | |
like other string methods we've seen, such as `upper` and `strip`: | |
>>> "32".isnumeric() | |
True | |
>>> "3.14159265358979".isnumeric() | |
False | |
As you can see it returns a True/False value corresponding simply to whether all character | |
points contained in a string correspond to decimal (0-9) numbers. Definitely a convenient | |
utility function for this exercise. (See also: `isalpha` and `isalnum` which work in much | |
the same way.) | |
---------------------------------------------------------------------- | |
Ran 12 tests in 6.818s | |
FAILED (failures=4) | |
""" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment