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 = ""
+ for error in errors:
+ result += "- " + error + "
"
+ 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