Skip to content

Commit

Permalink
Merge pull request #803 from robbrad/795_unit_test_coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
robbrad authored Sep 5, 2024
2 parents ab9d26b + 23c65ae commit 508c277
Show file tree
Hide file tree
Showing 6 changed files with 295 additions and 138 deletions.
16 changes: 16 additions & 0 deletions custom_components/uk_bin_collection/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,15 @@ async def get_council_schema(self, council=str) -> vol.Schema:
{vol.Optional("local_browser", default=False): bool}
)

# Add timeout field with default value of 60 seconds
council_schema = council_schema.extend(
{
vol.Optional("timeout", default=60): vol.All(
vol.Coerce(int), vol.Range(min=10)
)
}
)

return council_schema

async def async_step_user(self, user_input=None):
Expand Down Expand Up @@ -237,6 +246,13 @@ async def async_step_reconfigure_confirm(
)
added_fields.add("local_browser")

# Include the fields from existing_data that were present in the original config
if "timeout" in existing_data:
schema = schema.extend(
{vol.Required("timeout", default=existing_data["timeout"]): int}
)
added_fields.add("timeout")

# Add any other fields defined in council_schema that haven't been added yet
for key, field in council_schema.schema.items():
if key not in added_fields:
Expand Down
9 changes: 6 additions & 3 deletions custom_components/uk_bin_collection/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ async def async_setup_entry(
_LOGGER.info(LOG_PREFIX + "Data Supplied: %s", config.data)

name = config.data.get("name", "")
timeout = config.data.get("timeout", 60) # Get timeout from config or default to 60
args = [
config.data.get("council", ""),
config.data.get("url", ""),
Expand All @@ -60,6 +61,7 @@ async def async_setup_entry(
"skip_get_url",
"headless",
"local_browser",
"timeout",
}
),
]
Expand All @@ -86,7 +88,7 @@ async def async_setup_entry(
ukbcd.set_args(args)
_LOGGER.info(f"{LOG_PREFIX} Args set")

coordinator = HouseholdBinCoordinator(hass, ukbcd, name)
coordinator = HouseholdBinCoordinator(hass, ukbcd, name, timeout=timeout)

_LOGGER.info(f"{LOG_PREFIX} UKBinCollectionApp Init Refresh")
await coordinator.async_config_entry_first_refresh()
Expand Down Expand Up @@ -131,7 +133,7 @@ def get_latest_collection_info(data) -> dict:
class HouseholdBinCoordinator(DataUpdateCoordinator):
"""Household Bin Coordinator"""

def __init__(self, hass, ukbcd, name):
def __init__(self, hass, ukbcd, name, timeout=60):
"""Initialize my coordinator."""
super().__init__(
hass,
Expand All @@ -142,9 +144,10 @@ def __init__(self, hass, ukbcd, name):
_LOGGER.info(f"{LOG_PREFIX} UKBinCollectionApp Init")
self.ukbcd = ukbcd
self.name = name
self.timeout = timeout # Set the timeout value

async def _async_update_data(self):
async with async_timeout.timeout(60) as cm:
async with async_timeout.timeout(self.timeout) as cm:
_LOGGER.info(f"{LOG_PREFIX} UKBinCollectionApp Updating")

data = await self.hass.async_add_executor_job(self.ukbcd.run)
Expand Down
2 changes: 2 additions & 0 deletions custom_components/uk_bin_collection/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"title": "Provide council details",
"data": {
"url": "URL to fetch bin collection data",
"timeout": "The time in seconds for how long the sensor should wait for data",
"uprn": "UPRN (Unique Property Reference Number)",
"postcode": "Postcode of the address",
"number": "House number of the address",
Expand All @@ -29,6 +30,7 @@
"title": "Update council details",
"data": {
"url": "URL to fetch bin collection data",
"timeout": "The time in seconds for how long the sensor should wait for data",
"uprn": "UPRN (Unique Property Reference Number)",
"postcode": "Postcode of the address",
"number": "House number of the address",
Expand Down
2 changes: 2 additions & 0 deletions custom_components/uk_bin_collection/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"title": "Provide council details",
"data": {
"url": "URL to fetch bin collection data",
"timeout": "The time in seconds for how long the sensor should wait for data",
"uprn": "UPRN (Unique Property Reference Number)",
"postcode": "Postcode of the address",
"number": "House number of the address",
Expand All @@ -29,6 +30,7 @@
"title": "Update council details",
"data": {
"url": "URL to fetch bin collection data",
"timeout": "The time in seconds for how long the sensor should wait for data",
"uprn": "UPRN (Unique Property Reference Number)",
"postcode": "Postcode of the address",
"number": "House number of the address",
Expand Down
207 changes: 72 additions & 135 deletions uk_bin_collection/tests/test_collect_data.py
Original file line number Diff line number Diff line change
@@ -1,136 +1,73 @@
import json
from unittest import mock

from unittest.mock import MagicMock, patch
import argparse
import pytest
from requests import exceptions as req_exp
from requests.models import Response
from uk_bin_collection.get_bin_data import AbstractGetBinDataClass as agbdc
from uk_bin_collection.get_bin_data import setup_logging
import logging


def mocked_requests_get(*args, **kwargs):
class MockResponse:
def __init__(self, json_data, status_code, raise_error_type):
self.text = json_data
self.status_code = status_code
if raise_error_type is not None:
self.raise_for_status = self.raise_error(raise_error_type)
else:
self.raise_for_status = lambda: None

def raise_error(self, errorType):
if errorType == "HTTPError":
raise req_exp.HTTPError()
elif errorType == "ConnectionError":
raise req_exp.ConnectionError()
elif errorType == "Timeout":
raise req_exp.Timeout()
elif errorType == "RequestException":
raise req_exp.RequestException()
return errorType

if args[0] == "aurl":
return MockResponse({"test_data": "test"}, 200, None)
elif args[0] == "HTTPError":
return MockResponse({}, 999, "HTTPError")
elif args[0] == "ConnectionError":
return MockResponse({}, 999, "ConnectionError")
elif args[0] == "Timeout":
return MockResponse({}, 999, "Timeout")
elif args[0] == "RequestException":
return MockResponse({}, 999, "RequestException")
elif args[0] == "notPage":
return MockResponse("not json", 200, None)
return MockResponse(None, 404, "HTTPError")


# Unit tests


def test_logging_exception():
logging_dict = "SW1A 1AA"
with pytest.raises(ValueError) as exc_info:
result = setup_logging(logging_dict, "ROOT")
assert exc_info.typename == "ValueError"


def test_setup_logging_valid_config():
# Example of a minimal valid logging configuration dictionary
logging_config = {
"version": 1,
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "DEBUG",
},
},
"loggers": {
"ROOT": {
"handlers": ["console"],
"level": "DEBUG",
},
},
}
logger_name = "ROOT"
# Run the function with valid logging configuration
logger = setup_logging(logging_config, logger_name)

# Assert that logger is correctly configured
assert logger.name == logger_name
assert logger.level == logging.DEBUG


@mock.patch("requests.get", side_effect=mocked_requests_get)
def test_get_data(mock_get):
page_data = agbdc.get_data("aurl")
assert page_data.text == {"test_data": "test"}


@pytest.mark.parametrize(
"url", ["HTTPError", "ConnectionError", "Timeout", "RequestException"]
)
@mock.patch("requests.get", side_effect=mocked_requests_get)
def test_get_data_error(mock_get, url):
with pytest.raises(Exception) as exc_info:
result = agbdc.get_data(url)
assert exc_info.typename == url


def test_output_json():
bin_data = {"bin": ""}
output = agbdc.output_json(bin_data)
assert type(output) == str
assert output == '{\n "bin": ""\n}'

class ConcreteGetBinDataClass(agbdc):
"""Concrete implementation of the abstract class to test abstract methods."""
def parse_data(self, page: str, **kwargs) -> dict:
return {"mock_key": "mock_value"}

@pytest.fixture
def concrete_class_instance():
return ConcreteGetBinDataClass()

def test_get_and_parse_data_no_skip_get_url(concrete_class_instance):
mock_page = "mocked page content"
mock_parsed_data = {"mock_key": "mock_value"}

with mock.patch.object(concrete_class_instance, 'get_data', return_value=mock_page) as mock_get_data, \
mock.patch.object(concrete_class_instance, 'parse_data', return_value=mock_parsed_data) as mock_parse_data:

result = concrete_class_instance.get_and_parse_data("http://example.com")

mock_get_data.assert_called_once_with("http://example.com")
mock_parse_data.assert_called_once_with(mock_page, url="http://example.com")
assert result == mock_parsed_data

def test_get_and_parse_data_skip_get_url(concrete_class_instance):
mock_parsed_data = {"mock_key": "mock_value"}

with mock.patch.object(concrete_class_instance, 'parse_data', return_value=mock_parsed_data) as mock_parse_data:

result = concrete_class_instance.get_and_parse_data("http://example.com", skip_get_url=True)

mock_parse_data.assert_called_once_with("", url="http://example.com", skip_get_url=True)
assert result == mock_parsed_data
from uk_bin_collection.collect_data import UKBinCollectionApp, import_council_module



# Test UKBinCollectionApp setup_arg_parser
def test_setup_arg_parser():
app = UKBinCollectionApp()
app.setup_arg_parser()

# Assert that the argument parser has the correct arguments
assert isinstance(app.parser, argparse.ArgumentParser)
args = app.parser._actions
arg_names = [action.dest for action in args]

expected_args = [
"module",
"URL",
"postcode",
"number",
"skip_get_url",
"uprn",
"web_driver",
"headless",
"local_browser",
"dev_mode",
]
assert all(arg in arg_names for arg in expected_args)


# Test UKBinCollectionApp set_args
def test_set_args():
app = UKBinCollectionApp()
app.setup_arg_parser()

# Test valid args
args = ["council_module", "http://example.com", "--postcode", "AB1 2CD"]
app.set_args(args)

assert app.parsed_args.module == "council_module"
assert app.parsed_args.URL == "http://example.com"
assert app.parsed_args.postcode == "AB1 2CD"


# Test UKBinCollectionApp client_code method
def test_client_code():
app = UKBinCollectionApp()
mock_get_bin_data_class = MagicMock()

# Run the client_code and ensure that template_method is called
app.client_code(mock_get_bin_data_class, "http://example.com", postcode="AB1 2CD")
mock_get_bin_data_class.template_method.assert_called_once_with(
"http://example.com", postcode="AB1 2CD"
)


# Test the run() function with logging setup
@patch("uk_bin_collection.collect_data.setup_logging") # Correct patch path
@patch("uk_bin_collection.collect_data.UKBinCollectionApp.run") # Correct patch path
@patch("sys.argv", ["uk_bin_collection.py", "council_module", "http://example.com"])
def test_run_function(mock_app_run, mock_setup_logging):
from uk_bin_collection.collect_data import run

mock_setup_logging.return_value = MagicMock()
mock_app_run.return_value = None

run()

# Ensure logging was set up and the app run method was called
mock_setup_logging.assert_called_once()
mock_app_run.assert_called_once()
Loading

0 comments on commit 508c277

Please sign in to comment.