Folder structure:
.
│ .gitignore
│ LICENSE
│ README.md
│ requirements.txt ========> python dependencies, a MUST
│
├───.github
│ └───workflows
│ pythonapp.yml =======> CI workflow for python
│
├───qa327
│ │ app.py ===============> where we actually store the main function
│ │ __init__.py
│ └───__main__.py ==========> trigger by 'python -m qa327'
│
│
└───qa327_test
│ test_main_approach1.py
│ test_main_approach2.py
│ __init__.py
│
└───r2
terminal_input.txt
terminal_output_tail.txt
transaction_summary_file.txt
valid_account_list_file.txt
To run the application:
$ pip install -r requirements.txt
$ python -m qa327
To run all the test code:
$ pytest
This example app has three simple requirements:
import sys
import os
def main():
""" An example program of frontend that does
R1 program only accepts 'login' as key
R2 print valid_account_list_file's content
R3 write 'hmm i am a transaction.' to the transaction_summary_file
"""
print('Welcome the Queens ATM machine')
# for simplicity
# you can use argparse for sure
valid_account_list_file = sys.argv[1]
transaction_summary_file = sys.argv[2]
# R1:
user_input = input('what is the key?\n')
if(user_input == 'login'):
print('here is the content')
with open(valid_account_list_file) as rf:
print(rf.read())
print('writing transactions...')
with open(transaction_summary_file, 'w') as wf:
wf.write('hmm i am a transaction.')
# exit
else:
print('omg wrong key')
We created a helper function to assist testing. Let's create a test case cover R2 & R3:
Approach #1. We store all the input content (terminal & file) and all the output content (terminal & file) in a folder (r2 in this test case). The helper function looks up corresponding data from the folder.
import tempfile
from importlib import reload
import os
import io
import sys
import qa327.app as app
path = os.path.dirname(os.path.abspath(__file__))
def test_r2(capsys):
"""Testing r2. All required information stored in folder r2.
Arguments:
capsys -- object created by pytest to capture stdout and stderr
"""
helper(
capsys=capsys,
test_id='r2'
)
def helper(
capsys,
test_id):
""" a helper function that test requirements for the example app
Arguments:
capsys -- object created by pytest to capture stdout and stderr
"""
# cleanup package
reload(app)
# locate test case folder:
case_folder = os.path.join(path, test_id)
# read terminal input:
with open(
os.path.join(
case_folder, 'terminal_input.txt')) as rf:
terminal_input = rf.read().splitlines()
# read expected tail portion of the terminal output:
with open(
os.path.join(
case_folder, 'terminal_output_tail.txt')) as rf:
terminal_output_tail = rf.read().splitlines()
# create a temporary file in the system to store output transactions
temp_fd, temp_file = tempfile.mkstemp()
transaction_summary_file = temp_file
# prepare program parameters
sys.argv = [
'app.py',
os.path.join(case_folder, 'valid_account_list_file.txt'),
transaction_summary_file]
# set terminal input
sys.stdin = io.StringIO(
os.linesep.join(terminal_input))
# run the program
app.main()
# capture terminal output / errors
# assuming that in this case we don't use stderr
out, err = capsys.readouterr()
# split terminal output in lines
out_lines = out.splitlines()
# compare terminal outputs at the end.`
for i in range(1, len(terminal_output_tail)+1):
index = i * -1
assert terminal_output_tail[index] == out_lines[index]
# compare transactions:
with open(transaction_summary_file, 'r') as of:
content = of.read()
with open(os.path.join(case_folder, 'transaction_summary_file.txt'), 'r') as exp_file_of:
expected_content = exp_file_of.read()
assert content == expected_content
# clean up
os.close(temp_fd)
os.remove(temp_file)
Approach #2. We store all the input content (terminal & file) and all the output content (terminal & file) in the code. The helper function create temporary file on the system as needed for the program to run (e.g. valid_account_list_file).
import tempfile
from importlib import reload
import os
import io
import sys
import qa327.app as app
path = os.path.dirname(os.path.abspath(__file__))
def test_r2(capsys):
"""Testing r2. Self-contained (i.e. everything in the code approach)
[my favorite - all in one place with the code]
Arguments:
capsys -- object created by pytest to capture stdout and stderr
"""
helper(
capsys=capsys,
terminal_input=[
'login'
],
intput_valid_accounts=[
'123456'
],
expected_tail_of_terminal_output=[
'here is the content',
'123456',
'writing transactions...'],
expected_output_transactions=[
'hmm i am a transaction.'
]
)
def helper(
capsys,
terminal_input,
expected_tail_of_terminal_output,
intput_valid_accounts,
expected_output_transactions
):
"""Helper function for testing
Arguments:
capsys -- object created by pytest to capture stdout and stderr
terminal_input -- list of string for terminal input
expected_tail_of_terminal_output list of expected string at the tail of terminal
intput_valid_accounts -- list of valid accounts in the valid_account_list_file
expected_output_transactions -- list of expected output transactions
"""
# cleanup package
reload(app)
# create a temporary file in the system to store output transactions
temp_fd, temp_file = tempfile.mkstemp()
transaction_summary_file = temp_file
# create a temporary file in the system to store the valid accounts:
temp_fd2, temp_file2 = tempfile.mkstemp()
valid_account_list_file = temp_file2
with open(valid_account_list_file, 'w') as wf:
wf.write('\n'.join(intput_valid_accounts))
# prepare program parameters
sys.argv = [
'app.py',
valid_account_list_file,
transaction_summary_file]
# set terminal input
sys.stdin = io.StringIO(
os.linesep.join(terminal_input))
# run the program
app.main()
# capture terminal output / errors
# assuming that in this case we don't use stderr
out, err = capsys.readouterr()
# split terminal output in lines
out_lines = out.splitlines()
# compare terminal outputs at the end.`
for i in range(1, len(expected_tail_of_terminal_output)+1):
index = i * -1
assert expected_tail_of_terminal_output[index] == out_lines[index]
# compare transactions:
with open(transaction_summary_file, 'r') as of:
content = of.read().splitlines()
for ind in range(len(content)):
assert content[ind] == expected_output_transactions[ind]
# clean up
os.close(temp_fd)
os.remove(temp_file)