From 035d281727a9b382f3c2a715be54d98cb43c9da1 Mon Sep 17 00:00:00 2001 From: Hanne Heggdal Date: Sat, 19 Apr 2025 11:22:30 +0200 Subject: [PATCH 1/4] test - that data is clean --- tests/unit/test_clean_data.py | 103 ++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 tests/unit/test_clean_data.py diff --git a/tests/unit/test_clean_data.py b/tests/unit/test_clean_data.py new file mode 100644 index 0000000..066cd37 --- /dev/null +++ b/tests/unit/test_clean_data.py @@ -0,0 +1,103 @@ +import unittest +import pandas as pd +import numpy as np +from src.my_package.util import ( + kelvin_to_celsius, + ensure_rain_column, + ensure_snow_column, + fill_rain_column, + fill_snow_column, + extract_city_df +) + +class TestUtilFunctions(unittest.TestCase): + + # Test kelvin to celsius conversion + def test_kelvin_to_celsius(self): + kelvin_temp = 300 + expected_celsius = 26.85 + + result = kelvin_to_celsius(kelvin_temp) + + self.assertAlmostEqual(result, expected_celsius, places=2) + + # Test if 'rain.1h' column is added when not present + def test_ensure_rain_column(self): + df = pd.DataFrame({ + 'temp': [300, 302, 305], + 'humidity': [80, 82, 78] + }) + + df = ensure_rain_column(df) + + # Check if the 'rain.1h' column is present after function call + self.assertTrue('rain.1h' in df.columns) + # Check if the column has NaN values + self.assertTrue(df['rain.1h'].isna().all()) + + # Test if 'snow.1h' column is added when not present + def test_ensure_snow_column(self): + df = pd.DataFrame({ + 'temp': [300, 302, 305], + 'humidity': [80, 82, 78] + }) + + df = ensure_snow_column(df) + + # Check if the 'snow.1h' column is present after function call + self.assertTrue('snow.1h' in df.columns) + # Check if the column has NaN values + self.assertTrue(df['snow.1h'].isna().all()) + + # Test if NaN values in 'rain.1h' are filled with 0 + def test_fill_rain_column(self): + df = pd.DataFrame({ + 'temp': [300, 302, 305], + 'rain.1h': [np.nan, 1.0, np.nan] + }) + + df = fill_rain_column(df) + + # Check if NaN values are replaced with 0 + self.assertEqual(df['rain.1h'].iloc[0], 0) + self.assertEqual(df['rain.1h'].iloc[2], 0) + + # Test if NaN values in 'snow.1h' are filled with 0 + def test_fill_snow_column(self): + df = pd.DataFrame({ + 'temp': [300, 302, 305], + 'snow.1h': [np.nan, 0.5, np.nan] + }) + + df = fill_snow_column(df) + + # Check if NaN values are replaced with 0 + self.assertEqual(df['snow.1h'].iloc[0], 0) + self.assertEqual(df['snow.1h'].iloc[2], 0) + + # Test extracting city DataFrame from JSON data + def test_extract_city_df(self): + weather_data = { + "list": [ + {"dt": 1618245600, "temp": 290, "humidity": 85, "weather": [{"description": "clear sky"}]}, + {"dt": 1618255600, "temp": 295, "humidity": 80, "weather": [{"description": "cloudy"}]}, + {"dt": 1618265600, "temp": 300, "humidity": 75, "weather": [{"description": "sunny"}]} + ] + } + + df = extract_city_df(weather_data) + + # Ensure that 'dt' is the index and in datetime format + self.assertTrue(pd.api.types.is_datetime64_any_dtype(df.index)) + + # Check if duplicates are removed based on 'dt' + self.assertEqual(len(df), 3) + + # Check if 'weather' column was dropped + self.assertNotIn('weather', df.columns) + + # Check the correct conversion of 'dt' to datetime + self.assertEqual(df.index[0], pd.to_datetime(1618245600, unit='s')) + +if __name__ == '__main__': + unittest.main() From 75fbf2d02f27113aaecc8096f71cfe7e1b5ed02d Mon Sep 17 00:00:00 2001 From: Hanne Heggdal Date: Sat, 19 Apr 2025 11:22:57 +0200 Subject: [PATCH 2/4] test - simulate user input --- tests/unit/test_input.py | 51 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 tests/unit/test_input.py diff --git a/tests/unit/test_input.py b/tests/unit/test_input.py new file mode 100644 index 0000000..2f76b5c --- /dev/null +++ b/tests/unit/test_input.py @@ -0,0 +1,51 @@ +import unittest +from unittest.mock import patch +import datetime +import sys +import os +from src.my_package import date_to_unix # This is the module we are testing + +# This will make the absolute path from the root of the project, and will therefore work every time +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../../src"))) + + +#the goal for this test is to simulate user input and check if the returned UNIX timestamps are correct, for this we use mock input + +class TestDateToUnix(unittest.TestCase): + + @patch('builtins.input', side_effect=["2024, 4, 13, 12, 30", "2024, 4, 14, 15, 45"]) + def test_get_unix_timestamp(self, mock_input): + _ = mock_input + + # Expected UNIX timestamps for the hardcoded inputs + expected_start = int(datetime.datetime(2024, 4, 13, 12, 30).timestamp()) + expected_end = int(datetime.datetime(2024, 4, 14, 15, 45).timestamp()) + + # Call the function which uses input() — the @patch above mocks input to return predefined values + unix_start, unix_end = date_to_unix.get_unix_timestamp() + + # Check that the returned timestamps match the expected values + self.assertEqual(unix_start, expected_start) + self.assertEqual(unix_end, expected_end) + + # test the function to ensure it correctly convert timestamp back to datetime + def test_from_unix_timestamp(self): + + # Create known datetime objects + start_dt = datetime.datetime(2024, 4, 13, 12, 30) + end_dt = datetime.datetime(2024, 4, 14, 15, 45) + + # Convert these to UNIX timestamps + unix_start = int(start_dt.timestamp()) + unix_end = int(end_dt.timestamp()) + + # Use the function to convert them back to datetime + start_from_unix, end_from_unix = date_to_unix.from_unix_timestamp(unix_start, unix_end) + + # Assert that the conversion back to datetime matches the original values + self.assertEqual(start_from_unix, start_dt) + self.assertEqual(end_from_unix, end_dt) + +# Run all tests in this file when the script is executed directly +if __name__ == '__main__': + unittest.main() From dc6e2dfc849cf79e26f6c3a5e094ef673b600c5e Mon Sep 17 00:00:00 2001 From: Hanne Heggdal Date: Sat, 19 Apr 2025 11:23:26 +0200 Subject: [PATCH 3/4] test - consistent and mock --- tests/unit/test_format_yeardata.py | 82 ++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 tests/unit/test_format_yeardata.py diff --git a/tests/unit/test_format_yeardata.py b/tests/unit/test_format_yeardata.py new file mode 100644 index 0000000..7407682 --- /dev/null +++ b/tests/unit/test_format_yeardata.py @@ -0,0 +1,82 @@ +import unittest +import os +import requests +from unittest.mock import patch +from src.my_package.year_data import fetch_data + +class TestDataFormatConsistency(unittest.TestCase): + + @patch('requests.get') + def test_fetch_data_success(self, mock_get): + # Sample valid response data structure for the API + mock_data = { + 'city': { + 'name': 'Maura', + 'country': 'NO' + }, + 'data': [ + {'year': 2020, 'temperature': 12.5, 'precipitation': 100}, + {'year': 2021, 'temperature': 13.0, 'precipitation': 120}, + ] + } + + # Mock the API response + mock_response = unittest.mock.Mock() + mock_response.status_code = 200 + mock_response.json.return_value = mock_data + + mock_get.return_value = mock_response + + city_name = 'Maura' + data, folder = fetch_data(city_name) + + # Check if the data returned is a dictionary and contains the expected keys + self.assertIsInstance(data, dict) + self.assertIn('city', data) + self.assertIn('data', data) + + # Check if the 'city' key contains the expected structure + self.assertIn('name', data['city']) + self.assertIn('country', data['city']) + + # Check if the 'data' key contains a list of dictionaries with required fields + self.assertIsInstance(data['data'], list) + self.assertGreater(len(data['data']), 0) # Check that there is at least one year record + + for record in data['data']: + self.assertIn('year', record) + self.assertIn('temperature', record) + self.assertIn('precipitation', record) + self.assertIsInstance(record['year'], int) + self.assertIsInstance(record['temperature'], (int, float)) + self.assertIsInstance(record['precipitation'], (int, float)) + + # Test that the folder variable exists and is a valid path + self.assertEqual(folder, "../data/output_statistikk") + + @patch('requests.get') + def test_fetch_data_failure(self, mock_get): + # Mock an unsuccessful response (non-200 status code) + mock_response = unittest.mock.Mock() + mock_response.status_code = 404 + mock_response.json.return_value = {} + + mock_get.return_value = mock_response + + city_name = 'Maura' + data, folder = fetch_data(city_name) + + # Ensure data is empty and folder is correct even on failure + self.assertEqual(data, {}) + self.assertEqual(folder, "../data/output_statistikk") + + def test_api_key_in_env(self): + # Check if the API_KEY is loaded from the environment + api_key = os.getenv("API_KEY") + + self.assertIsNotNone(api_key, "API_KEY is not set in the environment.") + self.assertIsInstance(api_key, str) + self.assertGreater(len(api_key), 0, "API_KEY should not be an empty string.") + +if __name__ == '__main__': + unittest.main() From 1e2b69f5fd2a7e45ca1e58230b91c54485d9be71 Mon Sep 17 00:00:00 2001 From: Hanne Heggdal Date: Sat, 19 Apr 2025 11:24:08 +0200 Subject: [PATCH 4/4] test - consistent format, API --- tests/unit/test_format.py | 63 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/unit/test_format.py diff --git a/tests/unit/test_format.py b/tests/unit/test_format.py new file mode 100644 index 0000000..82be88f --- /dev/null +++ b/tests/unit/test_format.py @@ -0,0 +1,63 @@ +import unittest +import os + +# Function to check if the .env file is created correctly +def check_env_file_creation(): + env_filepath = os.path.join(os.path.dirname(__file__), "../../.env") + + # Check if the .env file exists + if not os.path.exists(env_filepath): + return False, f".env file does not exist at {env_filepath}" + + # Check if the file contains the expected keys + with open(env_filepath, 'r') as env_file: + file_content = env_file.read() + + if 'API_EMAIL' not in file_content or 'API_KEY' not in file_content: + return False, "The .env file is missing 'API_EMAIL' or 'API_KEY'." + + return True, ".env file is correctly created with API credentials." + +# Function to check if the API_EMAIL and API_KEY values are formatted correctly +def check_api_credentials(): + env_filepath = os.path.join(os.path.dirname(__file__), "../../.env") + + with open(env_filepath, 'r') as env_file: + lines = env_file.readlines() + + api_email = None + api_key = None + + for line in lines: + if 'API_EMAIL' in line: + api_email = line.strip().split('=')[1].strip().replace('"', '') + elif 'API_KEY' in line: + api_key = line.strip().split('=')[1].strip().replace('"', '') + + if not api_email or not api_key: + return False, "API_EMAIL or API_KEY is missing or formatted incorrectly." + + # Validate the email format (basic validation for '@' symbol) + if '@' not in api_email: + return False, "API_EMAIL format is incorrect." + + # Validate API_KEY length (assuming it's a standard length for OpenWeatherMap keys) + if len(api_key) < 32: # OpenWeatherMap keys are typically longer + return False, "API_KEY format is incorrect." + + return True, "API credentials are valid and properly formatted." + +class TestDataFormatConsistency(unittest.TestCase): + + # Test if the .env file is created correctly + def test_env_file_creation(self): + valid, message = check_env_file_creation() + self.assertTrue(valid, message) + + # Test if the API credentials are correct + def test_api_credentials(self): + valid, message = check_api_credentials() + self.assertTrue(valid, message) + +if __name__ == '__main__': + unittest.main()