From 2e4071b02e63cae75091e3bc462a74f721d1b72d Mon Sep 17 00:00:00 2001 From: Caleb Sitton Date: Wed, 26 Jun 2024 11:09:58 -0600 Subject: [PATCH] Added unit testing edits and README --- tests/unit_tests/README.md | 60 +++++++++++++++++++++++++ tests/unit_tests/test_heron.py | 80 ++++++++++------------------------ tests/unit_tests/tests | 15 +++++++ 3 files changed, 98 insertions(+), 57 deletions(-) diff --git a/tests/unit_tests/README.md b/tests/unit_tests/README.md index e69de29b..7bfbb374 100644 --- a/tests/unit_tests/README.md +++ b/tests/unit_tests/README.md @@ -0,0 +1,60 @@ +# Unit Testing +## HERON +The `test_heron.py` file contains unit tests for the `FORCE/src/heron/create_componentsets_in_HERON()` function. These are designed to help developers identify bugs and isolate their causes. To this end, a brief overview of the most important unique scenarios each unit test addresses is listed below. + +### TestMinimalInput +This test is designed to check main functionality with minimal edge cases. It checks that: +- New Component node was added and updated with content, including: + - economics node + - capex CashFlow, with all attributes + - reference price + - reference driver, with conversion from kW to mW + +### TestExpandedInput1 +This test considers a more complex HERON input XML. It checks that: +- Case node is transferred uncorrupted +- Contents of multiple existing components that should not be updated are uncorrupted +- New component's reference driver value is not converted if input is in mW +- DataGenerators node is transferred uncorrupted + +### TestExpandedInput2 +This test focuses on ensuring correct updating of economics and CashFlow nodes and subnodes. It checks that: +- Multiple components that need updating can be merged correctly with new components +- Existing CashFlow of type non-capex is uncorrupted +- capex CashFlow is added when a non-capex CashFlow exists but a capex CashFlow does not +- economics node is found successfully when non-economics subnode of the component precedes the economics subnode +- CashFlows are updated correctly when both a non-capex and a capex CashFlow exists +- Non-CashFlow subnode of economics node is uncorrupted and CashFlow is still found +- Existing capex CashFlow has + - Uncorrupted attributes + - Uncorrupted subnode that does not need updating + - All three existing subnodes that need updating correctly replaced +- CashFlow subnodes are updated correctly when two of the three types that need updating exist + +### TestNoComponentsNode +This test examines a single edge case where the Components node is missing from the HERON input XML script. It checks that: +- A new Components node is created if one does not exist +- The new Component node is placed within this new Components node + +### TestNoComponentNodes +This test examines the edge case where a Components node exists, but no Component nodes. It checks that: +- The new component is added, with content + +### TestMissingSubnodes +This test considers scenarios where a component that needs to be updated is missing an economics or CashFlow subnode. It checks that: +- If the economics node is missing, it is created with correct content +- If the CashFlow node is missing, it is created with correct content + +### TestEmptyCompSetsFolder +This test checks the function's behavior when the provided component set folder is empty. It checks that: +- The open function was not called (e.g., no file was opened) +- The existing component was not corrupted + +### TestCompSetsFolderWithBadJSON +This test ensures a correct response to a component set file that is not is proper JSON format. It checks that: +- The function throws a ValueError + +### TestCompSetsFolderMultFiles +This test checks the function's filtering system regarding which component set files it should open. It checks that: +- Only files whose names start with "componentSet" are opened +- Only files of type .txt or .json are opened \ No newline at end of file diff --git a/tests/unit_tests/test_heron.py b/tests/unit_tests/test_heron.py index a0615e09..40fe0cb9 100644 --- a/tests/unit_tests/test_heron.py +++ b/tests/unit_tests/test_heron.py @@ -336,7 +336,7 @@ def test_expanded_input_2(self, mock_listdir, mock_parse): ref_driver = cashflow[0].findall('./reference_driver') self.assertEqual(len(ref_driver), 1) ref_driver_value = ref_driver[0].findall('./fixed_value') - self.assertEqual(ref_driver_value.text, '3100') + self.assertEqual(ref_driver_value[0].text, '3100') # Reference price ref_price = cashflow[0].findall('./reference_price') @@ -376,13 +376,13 @@ def test_expanded_input_2(self, mock_listdir, mock_parse): ref_driver = cashflow[0].findall('./reference_driver') self.assertEqual(len(ref_driver), 1) ref_driver_value = ref_driver[0].findall('./fixed_value') - self.assertEqual(ref_driver_value.text, '4100') + self.assertEqual(ref_driver_value[0].text, '4100') # Reference price ref_price = cashflow[0].findall('./reference_price') self.assertEqual(len(ref_price), 1) ref_price_value = ref_price[0].findall('./fixed_value') - self.assertEqual(ref_price_value[0].text, '4200') + self.assertEqual(ref_price_value[0].text, '-4200') # Scaling factor scaling_factor = cashflow[0].findall('./scaling_factor_x') @@ -575,7 +575,6 @@ def test_empty_compsets_folder(self, mock_open, mock_listdir, mock_parse): self.assertEqual(len(component_nodes), 1) self.assertEqual(component_nodes[0].attrib['name'], 'ExistingComponent') -@unittest.skip("Waiting for function update (issue #18)") class TestCompSetsFolderWithBadJSON(unittest.TestCase): def setUp(self): # Example of a minimal XML structure @@ -605,13 +604,12 @@ def test_compsets_folder_bad_json(self, mock_open, mock_listdir, mock_parse): caught_bad_json = False try: result_tree = create_componentsets_in_HERON("/fake/folder", "/fake/heron_input.xml") - except ('json_read_error_name'): #FIXME: replace with correct error once function is updated + except ValueError: caught_bad_json = True with self.subTest("Did not respond correctly to bad component set file content"): self.assertEqual(caught_bad_json, True) -@unittest.skip("Waiting for function update (issue #18)") class TestCompSetsFolderMultFiles(unittest.TestCase): def setUp(self): # Example of a minimal XML structure @@ -629,54 +627,28 @@ def setUp(self): @patch('xml.etree.ElementTree.parse') @patch('os.listdir') - def test_compsets_folder_mult_files(self, mock_listdir, mock_parse): + # Open mock will return the same read_data each time it is called + # This is acceptable only because the content of the result tree is untested + @patch('builtins.open', + new_callable=mock_open, + read_data="""{ + "Component Set Name": "NewComponent", + "Reference Driver": 1000, + "Reference Driver Power Units": "mW", + "Reference Price (USD)": 2000, + "Scaling Factor": 0.5 + }""") + def test_compsets_folder_mult_files(self, mock_open, mock_listdir, mock_parse): # Set up the parse mock to return an XML tree mock_parse.return_value = self.tree # Only the txt and json files whose names start with 'componentSet' should be opened - files_list = ['component.json', 'componentSet.csv', 'componentSet.json', 'componentSetStuff.txt', - 'xcomponentSet.json', 'Set.json', 'compSet.json', 'ComponentSet.json'] + files_list = ['component.json', 'README', 'componentSet.csv', 'xcomponentSet.json', + 'componentSet.json', 'componentSetStuff.txt', + 'aFolder', 'Set.json', 'compSet.json', 'ComponentSet.json'] mock_listdir.return_value = files_list - # Set up the open mock to return different files each time it's used - # It should only be called twice, but it contains data for up to four reads - # So it won't break the function when an extra file or two is opened, and the unit tester can catch the bug - mock_open_mult = mock_open() - mock_open_mult.side_effect = [mock_open(read_data = - """{ - "Component Set Name": "NewComponent0", - "Reference Driver": 1000, - "Reference Driver Power Units": "mW", - "Reference Price (USD)": 1000, - "Scaling Factor": 0.1 - }""").return_value, - mock_open(read_data = - """{ - "Component Set Name": "NewComponent1", - "Reference Driver": 2100, - "Reference Driver Power Units": "mW", - "Reference Price (USD)": 2200, - "Scaling Factor": 0.2 - }""").return_value, - mock_open(read_data = - """{ - "Component Set Name": "NewComponent2", - "Reference Driver": 3100, - "Reference Driver Power Units": "mW", - "Reference Price (USD)": 3200, - "Scaling Factor": 0.3 - }""").return_value, - mock_open(read_data = - """{ - "Component Set Name": "NewComponent3", - "Reference Driver": 4100, - "Reference Driver Power Units": "mW", - "Reference Price (USD)": 4200, - "Scaling Factor": 0.4 - }""").return_value] - - # Call the function with patch for open function - with patch('builtins.open', mock_open_mult): - result_tree = create_componentsets_in_HERON("/fake/folder", "/fake/heron_input.xml") + # Call the function + result_tree = create_componentsets_in_HERON("/fake/folder", "/fake/heron_input.xml") # Verify open function was called on correct files for file in files_list: @@ -684,18 +656,12 @@ def test_compsets_folder_mult_files(self, mock_listdir, mock_parse): if file in ['componentSet.json', 'componentSetStuff.txt']: with self.subTest(msg="File was not opened and should have been", file = file): # Verify file was opened - self.assertIn(call('/fake/folder/'+file), mock_open_mult.call_args_list) + self.assertIn(call('/fake/folder/'+file), mock_open.call_args_list) # if file should not have been opened else: with self.subTest(msg="File was opened and should not have been", file = file): # Verify file was not opened - self.assertNotIn(call('/fake/folder/'+file), mock_open_mult.call_args_list) - - components_node = result_tree.find('./Components') - - # Verify component nodes were updated - component_nodes = components_node.findall('./Component') - self.assertEqual(len(component_nodes), 3) + self.assertNotIn(call('/fake/folder/'+file), mock_open.call_args_list) # This is not needed for running tests through FORCE/run_tests # It does allow tests to be run via the unit tester when test_heron is run directly diff --git a/tests/unit_tests/tests b/tests/unit_tests/tests index b0711ff0..ac522853 100644 --- a/tests/unit_tests/tests +++ b/tests/unit_tests/tests @@ -28,4 +28,19 @@ type = GenericExecutable executable = python -m unittest test_heron.TestMissingSubnodes [../] + + [./TestEmptyCompSetsFolder] + type = GenericExecutable + executable = python -m unittest test_heron.TestEmptyCompSetsFolder + [../] + + [./TestCompSetsFolderWithBadJSON] + type = GenericExecutable + executable = python -m unittest test_heron.TestCompSetsFolderWithBadJSON + [../] + + [./TestCompSetsFolderMultFiles] + type = GenericExecutable + executable = python -m unittest test_heron.TestCompSetsFolderMultFiles + [../] []