From 5442f1af4fffd20d98763b1e770006679ac3d5cd Mon Sep 17 00:00:00 2001 From: Despina Adamopoulou <16343312+despadam@users.noreply.github.com> Date: Fri, 3 May 2024 11:17:05 +0200 Subject: [PATCH] Functions: test keyword parameter (#222) * test keyword parameter * replace multiple asserts with if statements * update exercise 2 tests * format multi-line error * fix check errors * print multiple errors as list --- functions.ipynb | 6 +- tutorial/tests/test_functions.py | 145 +++++++++++++++++----------- tutorial/tests/testsuite/helpers.py | 2 +- 3 files changed, 92 insertions(+), 61 deletions(-) diff --git a/functions.ipynb b/functions.ipynb index 216f0bc..a27e4a6 100644 --- a/functions.ipynb +++ b/functions.ipynb @@ -473,7 +473,7 @@ "## Exercise 2\n", "\n", "Write a Python function called `calculate_area` that takes three parameters: `length` (a float), `width` (a float), and `unit` (a string with a **default** value of `\"cm\"`).\n", - "The function should calculate the area of a rectangle based on the given length and width, and return the result **as a tuple** with the correct, default unit (i.e., `cm^2`).\n", + "The function should calculate the area of a rectangle based on the given length and width, and return the result **as a string** including the correct, default unit (i.e., `cm^2`).\n", "If the unit parameter is \"m\", the function should convert the length and width from meters to centimeters before calculating the area.\n", "\n", "Your solution function **must** handle the following input units (the output unit is **always** `cm^2`):\n", @@ -1255,7 +1255,7 @@ "source": [ "\n", "
\n", - "

Question

How many valid password are there in your range?\n", + "

Question

How many valid passwords are there in your range?\n", "
" ] }, @@ -1306,7 +1306,7 @@ "Write a new function for validating password that includes the new rule.\n", "\n", "
\n", - "

Question

How many valid password are there in your range now?\n", + "

Question

How many valid passwords are there in your range now?\n", "
\n", "\n", "\n", diff --git a/tutorial/tests/test_functions.py b/tutorial/tests/test_functions.py index af52af6..1f49708 100644 --- a/tutorial/tests/test_functions.py +++ b/tutorial/tests/test_functions.py @@ -2,7 +2,7 @@ import pathlib from collections import Counter from string import ascii_lowercase, ascii_uppercase -from typing import Any, List, Tuple +from typing import Any, List import pytest @@ -12,6 +12,14 @@ def read_data(name: str, data_dir: str = "data") -> pathlib.Path: return (pathlib.Path(__file__).parent / f"{data_dir}/{name}").resolve() +def errors_to_list(errors): + result = "" + return result + + # # Exercise 1: a `greet` function # @@ -22,24 +30,38 @@ def reference_greet(name: str, age: int) -> str: return f"Hello, {name}! You are {age} years old." -def test_greet(function_to_test) -> None: - assert function_to_test.__doc__ is not None, "The function is missing a docstring" +@pytest.mark.parametrize( + "name,age", + [ + ("John", 30), + ], +) +def test_greet( + name: str, + age: int, + function_to_test, +) -> None: + errors = [] signature = inspect.signature(function_to_test) params = signature.parameters return_annotation = signature.return_annotation - assert len(params) == 2, "The function should take two arguments" - assert ( - "name" in params.keys() and "age" in params.keys() - ), "The function's parameters should be 'name' and 'age'" + if function_to_test.__doc__ is None: + errors.append("The function is missing a docstring.") + if len(params) != 2: + errors.append("The function should take two arguments.") + if "name" not in params.keys() or "age" not in params.keys(): + errors.append("The function's parameters should be 'name' and 'age'.") + if any(p.annotation == inspect.Parameter.empty for p in params.values()): + errors.append("The function's parameters should have type hints.") + if return_annotation == inspect.Signature.empty: + errors.append("The function's return value is missing the type hint.") - assert all( - p.annotation != inspect.Parameter.empty for p in params.values() - ), "The function's parameters should have type hints" - assert ( - return_annotation != inspect.Signature.empty - ), "The function's return value is missing the type hint" + # test signature + assert not errors, errors_to_list(errors) + # test result + assert function_to_test(name, age) == reference_greet(name, age) # @@ -47,9 +69,7 @@ def test_greet(function_to_test) -> None: # -def reference_calculate_area( - length: float, width: float, unit: str = "cm" -) -> Tuple[float, str] | str: +def reference_calculate_area(length: float, width: float, unit: str = "cm") -> str: """Reference solution for the calculate_area exercise""" # Conversion factors from supported units to centimeters units = { @@ -65,70 +85,81 @@ def reference_calculate_area( except KeyError: return f"Invalid unit: {unit}" else: - return (area, "cm^2") + return f"{area} cm^2" def test_calculate_area_signature(function_to_test) -> None: - assert function_to_test.__doc__ is not None, "The function is missing a docstring" + errors = [] signature = inspect.signature(function_to_test) params = signature.parameters return_annotation = signature.return_annotation - assert len(params) == 3, "The function should take three arguments" - assert ( - "length" in params.keys() - and "width" in params.keys() - and "unit" in params.keys() - ), "The function's parameters should be 'length', 'width' and 'unit'" - - assert all( - p.annotation != inspect.Parameter.empty for p in params.values() - ), "The function's parameters should have type hints" - assert ( - return_annotation != inspect.Signature.empty - ), "The function's return value is missing the type hint" + if function_to_test.__doc__ is None: + errors.append("The function is missing a docstring.") + if len(params) != 3: + errors.append("The function should take three arguments.") + if ( + "length" not in params.keys() + or "width" not in params.keys() + or "unit" not in params.keys() + ): + errors.append( + "The function's parameters should be 'length', 'width' and 'unit'." + ) + if "unit" in params.keys() and not ( + params["unit"].kind == inspect.Parameter.POSITIONAL_OR_KEYWORD + and params["unit"].default == "cm" + ): + errors.append("Argument 'unit' should have a default value 'cm'.") + if any(p.annotation == inspect.Parameter.empty for p in params.values()): + errors.append("The function's parameters should have type hints.") + if return_annotation == inspect.Signature.empty: + errors.append("The function's return value is missing the type hint.") + + assert not errors, errors_to_list(errors) @pytest.mark.parametrize( - "length,width,unit,expected", + "length,width,unit", [ - (2.0, 3.0, "cm", (6.0, "cm^2")), - (4.0, 5.0, "m", (200000.0, "cm^2")), - (10.0, 2.0, "mm", (2000.0, "cm^2")), - (2.0, 8.0, "yd", (133780.38, "cm^2")), - (5.0, 4.0, "ft", (18580.608, "cm^2")), - (3.0, 5.0, "in", (96.774, "cm^2")), + (2.0, 3.0, "cm"), + (4.0, 5.0, "m"), + (10.0, 2.0, "mm"), + (2.0, 8.0, "yd"), + (5.0, 4.0, "ft"), + (3.0, 5.0, "in"), ], ) def test_calculate_area_result( length: float, width: float, unit: str, - expected: Tuple[float, str], function_to_test, ) -> None: - result = function_to_test(length, width, unit) - test_result = reference_calculate_area(length, width, unit) + errors = [] if unit in ("cm", "m", "mm", "yd", "ft"): - assert isinstance(result, Tuple), "The function should return a tuple" - - assert "cm^2" in result, "The result should be in squared centimeters (cm^2)" - - # Double-check the reference solution - assert test_result[0] == pytest.approx( - expected[0], abs=0.01 - ), "The reference solution is incorrect" - - assert result[0] == pytest.approx(expected[0], abs=0.01) + result = function_to_test(length, width, unit) + + if not isinstance(result, str): + errors.append("The function should return a string.") + if "cm^2" not in result: + errors.append("The result should be in squared centimeters (cm^2).") + if result != reference_calculate_area(length, width, unit): + errors.append("The solution is incorrect.") else: - assert isinstance( - result, str - ), "The function should return an error string for unsupported units" - assert ( - result == f"Invalid unit: {unit}" - ), "The error message is incorrectly formatted" + try: + result = function_to_test(length, width, unit) + except KeyError: + errors.append( + "The function should return an error string for unsupported units." + ) + else: + if result != f"Invalid unit: {unit}": + errors.append("The error message is incorrectly formatted.") + + assert not errors # diff --git a/tutorial/tests/testsuite/helpers.py b/tutorial/tests/testsuite/helpers.py index d97a070..aab40e4 100644 --- a/tutorial/tests/testsuite/helpers.py +++ b/tutorial/tests/testsuite/helpers.py @@ -96,7 +96,7 @@ def format_error(exception: BaseException) -> str: ) # If we couldn't parse the exception message, just display it as is - formatted_message = formatted_message or f"

{html.escape(exception_str)}

" + formatted_message = formatted_message or f"

{exception_str}

" return formatted_message