Lecture Notes 8#
parametrized tests#
Consider
import green2red
def test_conversion_ratio_trivial():
calculated = green2red.get_conversion_ratio(3, 3, 40)
expected = 0
assert calculated == expected
def test_conversion_ratio():
calculated = green2red.get_conversion_ratio(3, 1.5, 40)
expected = 0.039
assert abs(calculated - expected) < .001
Instead of two testfunctions with explicit values we can introduce a parametrized test with the pytest framework
@pytest.mark.parametrize(
'req, avail, cream, repl',
[
(3, 3, 40, 0),
(3, 1.5, 40, 0.039),
]
)
def test_conversion_ratio(req, avail, cream, repl):
calculated = green2red.get_conversion_ratio(req, avail, cream)
expected = repl
assert abs(calculated - expected) < .001
The test values replace the arguments in the test function
Allows for generalization to testing many values without duplication code
Likewise, in the leap year example we have
>>> def test_leap_year():
... assert is_leap_year(2000) == True
... assert is_leap_year(1999) == False
... assert is_leap_year(1998) == False
... assert is_leap_year(1996) == True
... assert is_leap_year(1900) == False
... assert is_leap_year(1800) == False
... assert is_leap_year(1600) == True
vs
@pytest.mark.parametrize(
'year, is_leap',
[
(2000, True),
(1999, False),
(1998, False),
(1996, True),
(1900, False),
(1800, False),
(1600, True),
]
)
def test_leap_year(year, is_leap):
assert is_leap_year(year) == is_leap
Running the test suite in the latter case reports as if there had been separate test functions for each value
============================= test session starts ==============================
test_leap_year.py::test_leap_year[2000-True] PASSED [ 14%]
test_leap_year.py::test_leap_year[1999-False] PASSED [ 28%]
test_leap_year.py::test_leap_year[1998-False] PASSED [ 42%]
test_leap_year.py::test_leap_year[1996-True] PASSED [ 57%]
test_leap_year.py::test_leap_year[1900-False] PASSED [ 71%]
test_leap_year.py::test_leap_year[1800-False] PASSED [ 85%]
test_leap_year.py::test_leap_year[1600-True] PASSED [100%]
============================== 7 passed in 0.01s ===============================
test coverage#
used to measure how well code is tested
it requies an addition module, a pytest plugin
$ conda install pytest-cov
Now additional options are reported
$ pytest -v test_leap_year.py --cov leap_year
---------- coverage: platform linux, python 3.11.0-final-0 -----------
Name Stmts Miss Cover
----------------------------------
leap_year.py 4 0 100%
----------------------------------
TOTAL 4 0 100%
Sample exercises with pytest from codechalleng.es#
The tests contains pytest builtin tools,
during the tests pytest captures output from e.g. the print functions
in the test, it is possible to retrieve the printed text with the capfd fixture
test_driving.py#
from driving import allowed_driving
def test_not_allowed_to_drive(capfd):
allowed_driving('tim', 17)
output = capfd.readouterr()[0].strip()
assert output == 'tim is not allowed to drive'
def test_allowed_to_drive(capfd):
allowed_driving('bob', 18)
output = capfd.readouterr()[0].strip()
assert output == 'bob is allowed to drive'
def test_allowed_to_drive_other_name(capfd):
allowed_driving('julian', 19)
output = capfd.readouterr()[0].strip()
assert output == 'julian is allowed to drive'
driving.py#
MIN_DRIVING_AGE = 18
def allowed_driving(name, age):
"""Print '{name} is allowed to drive' or '{name} is not allowed to drive'
checking the passed in age against the MIN_DRIVING_AGE constant"""
if age < MIN_DRIVING_AGE:
print(f'{name} is not allowed to drive')
else:
print(f'{name} is allowed to drive')
============================= test session starts ==============================
...
test_driving.py::test_not_allowed_to_drive PASSED [ 33%]
test_driving.py::test_allowed_to_drive PASSED [ 66%]
test_driving.py::test_allowed_to_drive_other_name PASSED [100%]
============================== 3 passed in 0.01s ===============================
test_colors.py#
from unittest.mock import patch
from colors import print_colors
NOT_VALID = 'Not a valid color'
def call_print_colors():
# some people prefer sys.exit instead of break
try:
print_colors()
except SystemExit:
pass
@patch("builtins.input", side_effect=['quit'])
def test_straight_quit(input_mock, capsys):
# user only enter quit, program prints bye and breaks loop
call_print_colors()
actual = capsys.readouterr()[0].strip()
expected = 'bye'
assert actual == expected
@patch("builtins.input", side_effect=['blue', 'quit'])
def test_one_valid_color_then_quit(input_mock, capsys):
# user enters blue = valid color so print it
# then user enters quit so break out of loop = end program
call_print_colors()
actual = capsys.readouterr()[0].strip()
expected = 'blue\nbye'
assert actual == expected
@patch("builtins.input", side_effect=['Blue', 'quit'])
def test_title_cased_color_is_fine_too(input_mock, capsys):
# user enters Blue = valid color, program needs to lowercase it
# then user enters quit so break out of loop = end program
call_print_colors()
actual = capsys.readouterr()[0].strip()
expected = 'blue\nbye'
assert actual == expected
@patch("builtins.input", side_effect=['green', 'quit'])
def test_one_invalid_color_then_quit(input_mock, capsys):
# user enters green which is not in VALID_COLORS so continue the loop,
# user then enters quit so loop breaks (end function / program)
call_print_colors()
actual = capsys.readouterr()[0].strip()
expected = f'{NOT_VALID}\nbye'
assert actual == expected
@patch("builtins.input", side_effect=['white', 'red', 'quit'])
def test_invalid_then_valid_color_then_quit(nput_mock, capsys):
# white is not a valid color so continue the loop,
# then user enters red which is valid so print it, then quit
call_print_colors()
actual = capsys.readouterr()[0].strip()
expected = f'{NOT_VALID}\nred\nbye'
assert actual == expected
@patch("builtins.input", side_effect=['yellow', 'orange', 'quit'])
def test_valid_then_invalid_color_then_quit(input_mock, capsys):
# yellow is a valid color so print it, user then enters orange
# which is not a valid color so continue loop, lastly user
# enters quit so exit loop = reaching end function / program
call_print_colors()
actual = capsys.readouterr()[0].strip()
expected = f'yellow\n{NOT_VALID}\nbye'
assert actual == expected
Here we also have the
patch
function from the unittest.mock library.It replaces defined library calls in the tested function (here
input
: save command line entry)Solution is less complicated than the tests
An infinite loop asking for input is finished when the input value is quit
VALID_COLORS = ['blue', 'yellow', 'red']
def print_colors():
"""In the while loop ask the user to enter a color,
lowercase it and store it in a variable. Next check:
- if 'quit' was entered for color, print 'bye' and break.
- if the color is not in VALID_COLORS, print 'Not a valid color' and continue.
- otherwise print the color in lower case."""
while True:
color = input("Enter a color:").lower()
if color == 'quit':
print('bye')
break
elif color not in VALID_COLORS:
print("Not a valid color")
else:
print(color)
if __name__ == "__main__":
print_colors()
============================= test session starts ==============================
...
test_colors.py::test_straight_quit PASSED [ 16%]
test_colors.py::test_one_valid_color_then_quit PASSED [ 33%]
test_colors.py::test_title_cased_color_is_fine_too PASSED [ 50%]
test_colors.py::test_one_invalid_color_then_quit PASSED [ 66%]
test_colors.py::test_invalid_then_valid_color_then_quit PASSED [ 83%]
test_colors.py::test_valid_then_invalid_color_then_quit PASSED [100%]
============================== 6 passed in 0.01s ===============================
Running the code
$ python colors.py
Enter a color:red
red
Enter a color:Blue
blue
Enter a color:quit
bye