From f089317e865175974592c786c2b5b4067c7ab1f0 Mon Sep 17 00:00:00 2001 From: schaermicha1 Date: Fri, 27 Mar 2026 21:01:31 +0100 Subject: [PATCH] sw6 testing hands on --- .pre-commit-config.yaml | 2 +- src/exercises/pricing.py | 6 ++ src/testing/__init__.py | 0 src/testing/bmi.py | 16 +++ src/testing/catching_exeptions.py | 29 ++++++ src/testing/kata_list_filtering.py | 16 +++ src/testing/pandas_filter_rows.py | 41 ++++++++ src/testing/vowel_count.py | 18 ++++ tests/exercises/test_pricing.py | 28 ++++++ tests/test_moduleA.py | 2 +- tests/testing/test_catching_exceptions.py | 25 +++++ tests/testing/test_kata_list_filtering.py | 18 ++++ tests/testing/test_pandas_filter_rows.py | 115 ++++++++++++++++++++++ 13 files changed, 314 insertions(+), 2 deletions(-) create mode 100644 src/exercises/pricing.py create mode 100644 src/testing/__init__.py create mode 100644 src/testing/bmi.py create mode 100644 src/testing/catching_exeptions.py create mode 100644 src/testing/kata_list_filtering.py create mode 100644 src/testing/pandas_filter_rows.py create mode 100644 src/testing/vowel_count.py create mode 100644 tests/exercises/test_pricing.py create mode 100644 tests/testing/test_catching_exceptions.py create mode 100644 tests/testing/test_kata_list_filtering.py create mode 100644 tests/testing/test_pandas_filter_rows.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a00cee1..6078754 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,6 +13,6 @@ repos: - id: ruff # Linting - repo: https://github.com/psf/black - rev: stable + rev: 26.3.1 hooks: - id: black # formatting diff --git a/src/exercises/pricing.py b/src/exercises/pricing.py new file mode 100644 index 0000000..6d89ecd --- /dev/null +++ b/src/exercises/pricing.py @@ -0,0 +1,6 @@ +def discount_price(price: float, discount: float) -> float: + if price < 0: + raise ValueError("price cannot be negative") + if not 0 <= discount <= 100: + raise ValueError("discount must be between 0 and 100") + return price - (price * discount / 100) diff --git a/src/testing/__init__.py b/src/testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/testing/bmi.py b/src/testing/bmi.py new file mode 100644 index 0000000..51086c9 --- /dev/null +++ b/src/testing/bmi.py @@ -0,0 +1,16 @@ +# Task: Schreibt für die folgende Aufgabe einige Unit-Test. Nutzt dazu für den Happy-Path einen parametrisierten Test für +# einige valide Inputs und Outputs + + +def calculate_bmi(weight_kg: float, height_m: float) -> float: + """ + Berechnet den Body Mass Index (BMI). + BMI = Gewicht (kg) / Grösse (m)^2 + """ + if weight_kg <= 0 or height_m <= 0: + raise ValueError("Gewicht und Grösse müssen positiv sein.") + return weight_kg / (height_m**2) + + +if __name__ == "__main__": + print(calculate_bmi(weight_kg=72, height_m=1.84)) diff --git a/src/testing/catching_exeptions.py b/src/testing/catching_exeptions.py new file mode 100644 index 0000000..b040155 --- /dev/null +++ b/src/testing/catching_exeptions.py @@ -0,0 +1,29 @@ +# TASK: Schreibe sinnvolle Tests für die folgende Funktion +# Schreibe Tests für die korrekte Struktur des Outputs der Funktion +# Teste, ob bei Übergabe von 'falschem' Parameter data eine Exception geworfen wird + +# https://docs.python.org/3/library/exceptions.html +# Passing arguments of the wrong type (e.g. passing a list when an int is expected) should result in a TypeError, +# but passing arguments with the wrong value (e.g. a number outside expected boundaries) should result in a ValueError. + + +def double_integers(data: list[int]) -> list[int]: + """Doubles a list of given integers + + :param data: list of integers + :return: list of doubled integers + """ + + if not isinstance(data, list): + raise TypeError("data must be a list") + if not all([isinstance(i, int) for i in data]): + raise TypeError("data may contain only integers") + + return [i * 2 for i in data] + + +if __name__ == "__main__": + + data = [1, 2, 3] + result = double_integers(data) + print(result) diff --git a/src/testing/kata_list_filtering.py b/src/testing/kata_list_filtering.py new file mode 100644 index 0000000..97db40b --- /dev/null +++ b/src/testing/kata_list_filtering.py @@ -0,0 +1,16 @@ +# https://www.codewars.com/kata/53dbd5315a3c69eed20002dd +# level: 7 kyu + +# In this kata you will create a function that takes a list of non-negative integers and strings and returns a new +# list with the strings filtered out. +# +# Example +# filter_list([1,2,'a','b']) == [1,2] +# filter_list([1,'a','b',0,15]) == [1,0,15] +# filter_list([1,2,'aasf','1','123',123]) == [1,2,123] + + +def filter_list(input_list): + if not isinstance(input_list, list): + return [] + return [i for i in input_list if not isinstance(i, bool) and isinstance(i, int)] diff --git a/src/testing/pandas_filter_rows.py b/src/testing/pandas_filter_rows.py new file mode 100644 index 0000000..c929df1 --- /dev/null +++ b/src/testing/pandas_filter_rows.py @@ -0,0 +1,41 @@ +# https://www.codewars.com/kata/5ea2baed9345eb001e8ce394 +# 7 kyu + +# Input parameters + +# dataframe: pandas.DataFrame object +# col: target column +# func: filter function + +# Task +# Your function must return a new pandas.DataFrame object with the same columns as the original input. However, +# include only the rows whose cell values in the designated column evaluate to False by func. +# +# Input DataFrame will never be empty. The target column will always be one of the dataframe columns. Filter function +# will be a valid one. Index value must remain the same. + +# REMARK: leichte Modifikation -> es wird ausgegeben, was in der Funktion definiert wurde (True) und nicht umgekehrt. + +import pandas as pd + + +def filter_dataframe(df, col, func): + if col not in df.columns: + raise ValueError(f"Column '{col}' is not present in the DataFrame.") + mask = df[col].apply(func) + return df[mask] + + +if __name__ == "__main__": + + df = pd.DataFrame( + { + "A": list(range(0, 10)), + "B": list(range(-5, 5)), + "C": list(range(-2, 8)), + "D": list(range(10, 20)), + "E": list(range(-20, -10)), + } + ) + + print(filter_dataframe(df, "A", lambda x: x >= 4)) diff --git a/src/testing/vowel_count.py b/src/testing/vowel_count.py new file mode 100644 index 0000000..0cbbe2b --- /dev/null +++ b/src/testing/vowel_count.py @@ -0,0 +1,18 @@ +# https://www.codewars.com/kata/54ff3102c1bad923760001f3 +# level: 7 kyu + +# Return the number (count) of vowels in the given string. +# We will consider a, e, i, o, u as vowels for this Kata (but not y). +# The input string will only consist of lower case letters and/or spaces. + + +def get_count(inputStr): + num_vowels = 0 + for char in inputStr: + if char in "aeiouAEIOU": + num_vowels = num_vowels + 1 + return num_vowels + + +if __name__ == "__main__": + print(get_count("Hello World")) diff --git a/tests/exercises/test_pricing.py b/tests/exercises/test_pricing.py new file mode 100644 index 0000000..cadab9b --- /dev/null +++ b/tests/exercises/test_pricing.py @@ -0,0 +1,28 @@ +import pytest + +from src.exercises.pricing import discount_price + + +def test_discount_price_happy_path(): + result = discount_price(100.0, 20.0) + assert result == 80.0 + + +def test_discount_price_edge_case_1(): + result = discount_price(100.0, 100.0) + assert result == 0.0 + + +def test_discount_price_edge_case_2(): + result = discount_price(100.0, 0.0) + assert result == 100.0 + + +def test_discount_price_negative_price(): + with pytest.raises(ValueError, match="price cannot be negative"): + discount_price(-1.0, 20.0) + + +def test_discount_price_invalid_discount(): + with pytest.raises(ValueError, match="discount must be between 0 and 100"): + discount_price(100.0, 120.0) diff --git a/tests/test_moduleA.py b/tests/test_moduleA.py index 42fe6cc..e4e30e6 100644 --- a/tests/test_moduleA.py +++ b/tests/test_moduleA.py @@ -1,4 +1,4 @@ -from src.moduleA import addition +from src.exercises.moduleA import addition def test_a(): diff --git a/tests/testing/test_catching_exceptions.py b/tests/testing/test_catching_exceptions.py new file mode 100644 index 0000000..9a4a573 --- /dev/null +++ b/tests/testing/test_catching_exceptions.py @@ -0,0 +1,25 @@ +import pytest +from testing.catching_exeptions import double_integers + + +@pytest.mark.parametrize( + "data, expected_error", + [ + (123, "data must be a list"), + ([1, "2", 3, 4, 5], "data may contain only integers"), + ], +) +def test_double_integers_unhappy_path(data, expected_error) -> None: + with pytest.raises(TypeError, match=expected_error): + double_integers(data) + + +@pytest.mark.parametrize( + "data, expected_result", + [ + ([1, 2, 3, 4, 5], [2, 4, 6, 8, 10]), + ([0, -1, -2, -3, -4, -5], [0, -2, -4, -6, -8, -10]), + ], +) +def test_double_integers_happy_path(data, expected_result) -> None: + assert double_integers(data) == expected_result diff --git a/tests/testing/test_kata_list_filtering.py b/tests/testing/test_kata_list_filtering.py new file mode 100644 index 0000000..93b1620 --- /dev/null +++ b/tests/testing/test_kata_list_filtering.py @@ -0,0 +1,18 @@ +import pytest +from testing.kata_list_filtering import filter_list + + +@pytest.mark.parametrize( + "input_list,expected_result", + [ + ([1, 2, 3, 4, 5], [1, 2, 3, 4, 5]), + ([], []), + ([1 / 2, 1, "2", 3, "4", 5], [1, 3, 5]), + (["1", "2", "3", "4", "5"], []), + ([True, False], []), + (1, []), + ("asd123", []), + ], +) +def test_filter_list(input_list, expected_result) -> None: + assert filter_list(input_list) == expected_result diff --git a/tests/testing/test_pandas_filter_rows.py b/tests/testing/test_pandas_filter_rows.py new file mode 100644 index 0000000..ef772a8 --- /dev/null +++ b/tests/testing/test_pandas_filter_rows.py @@ -0,0 +1,115 @@ +import pytest +import pandas as pd +from testing.pandas_filter_rows import filter_dataframe + +# --- Fixtures --- + + +@pytest.fixture +def sample_df(): + return pd.DataFrame( + { + "age": [10, 25, 35, 45], + "name": ["Alice", "Bob", "Charlie", "Diana"], + "score": [88.5, 92.0, 70.0, 55.5], + } + ) + + +# --- Happy path tests --- + + +def test_filter_numeric_column(sample_df): + result = filter_dataframe(sample_df, "age", lambda x: x > 20) + assert list(result["age"]) == [25, 35, 45] + + +def test_filter_returns_dataframe(sample_df): + result = filter_dataframe(sample_df, "age", lambda x: x > 0) + assert isinstance(result, pd.DataFrame) + + +def test_filter_string_column(sample_df): + result = filter_dataframe(sample_df, "name", lambda x: x.startswith("A")) + assert list(result["name"]) == ["Alice"] + + +def test_filter_float_column(sample_df): + result = filter_dataframe(sample_df, "score", lambda x: x >= 88.5) + assert list(result["score"]) == [88.5, 92.0] + + +def test_filter_preserves_all_columns(sample_df): + result = filter_dataframe(sample_df, "age", lambda x: x == 25) + assert list(result.columns) == ["age", "name", "score"] + assert result.iloc[0]["name"] == "Bob" + + +def test_filter_preserves_original_index(sample_df): + result = filter_dataframe(sample_df, "age", lambda x: x > 30) + assert list(result.index) == [2, 3] + + +def test_filter_does_not_mutate_original(sample_df): + original_len = len(sample_df) + filter_dataframe(sample_df, "age", lambda x: x > 20) + assert len(sample_df) == original_len + + +# --- Edge cases --- + + +def test_filter_all_rows_pass(sample_df): + result = filter_dataframe(sample_df, "age", lambda x: x > 0) + assert len(result) == len(sample_df) + + +def test_filter_no_rows_pass(sample_df): + result = filter_dataframe(sample_df, "age", lambda x: x > 1000) + assert len(result) == 0 + assert list(result.columns) == ["age", "name", "score"] + + +def test_filter_on_empty_dataframe(): + empty_df = pd.DataFrame({"age": [], "name": []}) + result = filter_dataframe(empty_df, "age", lambda x: x > 10) + assert len(result) == 0 + + +def test_filter_with_none_values(): + df = pd.DataFrame({"value": [1, None, 3, None]}) + result = filter_dataframe(df, "value", lambda x: x is not None and x > 1) + assert list(result["value"]) == [3.0] + + +def test_filter_single_row_match(): + df = pd.DataFrame({"x": [42]}) + result = filter_dataframe(df, "x", lambda x: x == 42) + assert len(result) == 1 + + +def test_filter_single_row_no_match(): + df = pd.DataFrame({"x": [42]}) + result = filter_dataframe(df, "x", lambda x: x == 0) + assert len(result) == 0 + + +# --- Error handling --- + + +def test_missing_column_raises_value_error(sample_df): + with pytest.raises(ValueError, match="Column 'missing' is not present"): + filter_dataframe(sample_df, "missing", lambda x: x > 0) + + +def test_missing_column_error_message_includes_column_name(sample_df): + with pytest.raises(ValueError, match="nonexistent"): + filter_dataframe(sample_df, "nonexistent", lambda x: True) + + +def test_func_exception_propagates(sample_df): + def bad_func(x): + raise RuntimeError("Intentional error") + + with pytest.raises(RuntimeError, match="Intentional error"): + filter_dataframe(sample_df, "age", bad_func)