Skip to content

Instantly share code, notes, and snippets.

@ProbonoBonobo
Created September 29, 2021 18:34
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ProbonoBonobo/a424fc5e7733cec5865c17918b976a6a to your computer and use it in GitHub Desktop.
Save ProbonoBonobo/a424fc5e7733cec5865c17918b976a6a to your computer and use it in GitHub Desktop.
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