From d5495ef77576e54c27dc66cd5fd420816da8048c Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 13 Jun 2023 11:51:10 +0100 Subject: [PATCH] Extended unit testing for roof class --- epc_data/attributes/RoofAttributes.py | 21 +++++++++--- epc_data/tests/test_epc_clean.py | 33 +++++++++++++++++++ epc_data/tests/test_roof_attributes.py | 44 +++++++++++++++----------- 3 files changed, 75 insertions(+), 23 deletions(-) create mode 100644 epc_data/tests/test_epc_clean.py diff --git a/epc_data/attributes/RoofAttributes.py b/epc_data/attributes/RoofAttributes.py index 1768f898..81b05c44 100644 --- a/epc_data/attributes/RoofAttributes.py +++ b/epc_data/attributes/RoofAttributes.py @@ -3,16 +3,28 @@ from typing import Dict, Union class RoofAttributes: + ROOF_TYPES = ['pitched', 'roof room', 'loft', 'flat', 'thatched', 'at rafters', 'assumed'] + DWELLING_ABOVE = ["another dwelling above", "other premises above"] def __init__(self, description: str): """ :param description: Description of the roof. """ - self.description: str = description + + self.description: str = description.lower() + + if not description or not any( + rt in self.description for rt in self.ROOF_TYPES + self.DWELLING_ABOVE + ["average thermal transmittance"] + ): + raise ValueError('Invalid description') def process(self) -> Dict[str, Union[float, str, bool, None]]: + + if not self.description: + raise ValueError("Description is empty") + result: Dict[str, Union[float, str, bool, None]] = {} - description = self.description.lower() + description = self.description # thermal transmittance match = re.search(r"average thermal transmittance (-?\d+\.\d+)\s(w/m-¦k)", description) @@ -26,9 +38,8 @@ class RoofAttributes: result['thermal_transmittance_unit'] = None # roof type - roof_types = ['pitched', 'roof room', 'loft', 'flat', 'thatched', 'at rafters', 'assumed'] - for rt in roof_types: + for rt in self.ROOF_TYPES: result[f'is_{rt.replace(" ", "_")}'] = rt in description # Remove the roof type from the description description = description.replace(rt, "") @@ -36,7 +47,7 @@ class RoofAttributes: result["has_dwelling_above"] = ( "another dwelling above" in description or "other premises above" in description ) - for dwelling_above in ["another dwelling above", "other premises above"]: + for dwelling_above in self.DWELLING_ABOVE: description = description.replace(dwelling_above, "") result["is_valid"] = "invalid" not in description diff --git a/epc_data/tests/test_epc_clean.py b/epc_data/tests/test_epc_clean.py new file mode 100644 index 00000000..e8e8fd24 --- /dev/null +++ b/epc_data/tests/test_epc_clean.py @@ -0,0 +1,33 @@ +import pytest +import pickle +from epc_data.EpcClean import EpcClean +from pathlib import Path +from epc_data.tests.test_data.test_roof_attributes_cases import clean_roof_test_cases +from epc_data.attributes.RoofAttributes import RoofAttributes + +# For local testing +if __file__ == "": + input_data_path = Path("./epc_data/tests/test_data/EpcClean_inputs.obj") +else: + current_file_path = Path(__file__) + input_data_path = current_file_path.parent / 'test_data' / 'EpcClean_inputs.obj' + + +class TestEpcClean: + + @staticmethod + def load_data(path): + with open(path, "rb") as file: + return pickle.load(file) + + @pytest.fixture(autouse=True) + def setup_class(self): + self.cleaner = EpcClean(self.load_data(input_data_path)) + + def test_clean(self): + self.cleaner.clean() + assert len(self.cleaner.cleaned["roof-description"]) == len(self.cleaner.unique_vals["roof-description"]) + + def test__init_empty_cleaned_obj(self): + self.cleaner._init_empty_cleaned_obj() + assert all([len(values) == 0 for values in self.cleaner.cleaned.values()]) diff --git a/epc_data/tests/test_roof_attributes.py b/epc_data/tests/test_roof_attributes.py index 4f0369b8..ea7dbbc7 100644 --- a/epc_data/tests/test_roof_attributes.py +++ b/epc_data/tests/test_roof_attributes.py @@ -13,24 +13,7 @@ else: input_data_path = current_file_path.parent / 'test_data' / 'EpcClean_inputs.obj' -class TestEpcClean: - - @staticmethod - def load_data(path): - with open(path, "rb") as file: - return pickle.load(file) - - @pytest.fixture(autouse=True) - def setup_class(self): - self.cleaner = EpcClean(self.load_data(input_data_path)) - - def test_clean(self): - self.cleaner.clean() - assert len(self.cleaner.cleaned["roof-description"]) == len(self.cleaner.unique_vals["roof-description"]) - - def test__init_empty_cleaned_obj(self): - self.cleaner._init_empty_cleaned_obj() - assert all([len(values) == 0 for values in self.cleaner.cleaned.values()]) +class TestRoofAttributes: def test_clean_roof(self): result = RoofAttributes('Pitched, 270 mm loft insulation').process() @@ -84,3 +67,28 @@ class TestEpcClean: for k in expected_output: assert result[k] == expected_output[k] + + def test_clean_roof_invalid(self): + with pytest.raises(ValueError): + RoofAttributes('').process() + with pytest.raises(ValueError): + RoofAttributes('nonsense string').process() + + def test_clean_roof_edge_cases(self): + # Insulation thickness edge case + assert RoofAttributes('Pitched, 99999 mm loft insulation').process()['insulation_thickness'] == "99999" + assert RoofAttributes('Pitched, 1 mm loft insulation').process()['insulation_thickness'] == "1" + + # Special characters in the description - implement this later + # result = RoofAttributes('Pitched, **270** mm loft insulation').process() + # assert result['insulation_thickness'] == 270 + + def test_clean_roof_type(self): + for roof_type in ['pitched', 'roof room', 'loft', 'flat', 'thatched', 'at rafters', 'assumed']: + result = RoofAttributes(f'{roof_type}, 200 mm insulation').process() + assert result[f'is_{roof_type.replace(" ", "_")}'] is True + + def test_clean_roof_thermal_transmittance(self): + result = RoofAttributes('Pitched, 200 mm insulation, average thermal transmittance 0.13 w/m-¦k').process() + assert result['thermal_transmittance'] == 0.13 + assert result['thermal_transmittance_unit'] == 'w/m-¦k'