Repository: MGough/sensorhub

Issue:
Refactor class to use dependency injection & remove mock usage
This would be nicer with dependency injection of SMBus, and a dummy object used in the tests, rather than a mock.

Discussion:

Patch:
diff --git a/sensorhub/hub.py b/sensorhub/hub.py
index c5b0f31..d1c72d8 100644
--- a/sensorhub/hub.py
+++ b/sensorhub/hub.py
@@ -29,13 +29,13 @@ class StatusRegisterErrorCode(Enum):
 
 
 class SensorHub:
-    bus: SMBus
+    _bus: SMBus
 
-    def __init__(self):
-        self.bus = SMBus(DEVICE_BUS)
+    def __init__(self, system_management_bus: SMBus):
+        self._bus = system_management_bus or SMBus(DEVICE_BUS)
 
     def _read_sensor_board_register(self, buffer: SensorRegister) -> int:
-        return self.bus.read_byte_data(DEVICE_ADDR, buffer.value)
+        return self._bus.read_byte_data(DEVICE_ADDR, buffer.value)
 
     def _get_error_codes(self) -> int:
         """
diff --git a/setup.py b/setup.py
index 581900e..5b1420c 100644
--- a/setup.py
+++ b/setup.py
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
 
 setuptools.setup(
     name="sensorhub",
-    version="1.0.3",
+    version="2.0.0",
     author="Merlin Gough",
     author_email="goughmerlin@gmail.com",
     description="A simple library to use with the DockerPi SensorHub (EP-0106)",
@@ -14,7 +14,7 @@ setuptools.setup(
     url="https://github.com/MGough/sensorhub",
     packages=["sensorhub"],
     install_requires=["smbus2>=0.3.0"],
-    tests_require=["pytest>=5.3.5"],
+    tests_require=["pytest>=5.4.3"],
     classifiers=[
         "Programming Language :: Python :: 3",
         "License :: OSI Approved :: MIT License",


Test patch:
diff --git a/test/test_hub.py b/test/test_hub.py
index 03e45a2..4d639e6 100644
--- a/test/test_hub.py
+++ b/test/test_hub.py
@@ -1,11 +1,9 @@
 from pytest import fixture, raises, mark
-from unittest.mock import patch, call
+from unittest.mock import call, Mock
 
-from sensorhub.hub import SensorHub, SensorRegister
+from smbus2 import SMBus
 
-@fixture
-def device_bus():
-    return 1
+from sensorhub.hub import SensorHub, SensorRegister
 
 
 @fixture
@@ -14,17 +12,13 @@ def device_address():
 
 
 @fixture
-def sensor_hub():
-    with patch("sensorhub.hub.SMBus", autospec=True):
-        return SensorHub()
-
+def bus():
+    return Mock(SMBus, autospec=True)
 
-def test_correct_bus_is_created(device_bus):
-    with patch("sensorhub.hub.SMBus", autospec=True) as bus:
-        sensor_hub = SensorHub()
 
-        bus.assert_called_once_with(device_bus)
-        assert sensor_hub.bus == bus()
+@fixture
+def sensor_hub(bus):
+    return SensorHub(bus)
 
 
 @mark.parametrize("error_code", [
@@ -33,12 +27,12 @@ def test_correct_bus_is_created(device_bus):
     0b1001,
     0b1101
 ])
-def test_off_board_temperature_out_of_range_returns_minus_1(error_code, sensor_hub, device_address):
-    sensor_hub.bus.read_byte_data.return_value = 1
+def test_off_board_temperature_out_of_range_returns_minus_1(error_code, sensor_hub, bus, device_address):
+    bus.read_byte_data.return_value = 1
 
     temperature = sensor_hub.get_off_board_temperature()
 
-    sensor_hub.bus.read_byte_data.assert_called_once_with(device_address, SensorRegister.STATUS.value)
+    bus.read_byte_data.assert_called_once_with(device_address, SensorRegister.STATUS.value)
     assert temperature == -1
 
 
@@ -48,13 +42,13 @@ def test_off_board_temperature_out_of_range_returns_minus_1(error_code, sensor_h
     0b1110,
     0b1010
 ])
-def test_off_board_temperature_sensor_io_error(error_code, sensor_hub, device_address):
-    sensor_hub.bus.read_byte_data.return_value = error_code
+def test_off_board_temperature_sensor_io_error(error_code, sensor_hub, bus, device_address):
+    bus.read_byte_data.return_value = error_code
 
     with raises(IOError, match="Sensor Missing"):
         sensor_hub.get_off_board_temperature()
 
-    sensor_hub.bus.read_byte_data.assert_called_once_with(device_address, SensorRegister.STATUS.value)
+    bus.read_byte_data.assert_called_once_with(device_address, SensorRegister.STATUS.value)
 
 
 @mark.parametrize("error_code", [
@@ -63,67 +57,72 @@ def test_off_board_temperature_sensor_io_error(error_code, sensor_hub, device_ad
     0b1000,      # brightness sensor error
     0b1100       # brightness out of range AND sensor error (just in case...)
 ])
-def test_off_board_temperature_sensor_returns_temperature(error_code, sensor_hub, device_address):
+def test_off_board_temperature_sensor_returns_temperature(error_code, sensor_hub, bus, device_address):
     expected_temperature = 9001
-    sensor_hub.bus.read_byte_data.side_effect = [error_code, 9001]
+    bus.read_byte_data.side_effect = [error_code, 9001]
 
     temperature = sensor_hub.get_off_board_temperature()
 
     assert temperature == expected_temperature
-    sensor_hub.bus.read_byte_data.assert_has_calls([
+    bus.read_byte_data.assert_has_calls([
         call(device_address, SensorRegister.STATUS.value),
         call(device_address, SensorRegister.OFF_BOARD_TEMPERATURE.value)
     ])
 
 
-def test_humidity_is_not_up_to_date(sensor_hub, device_address):
-    sensor_hub.bus.read_byte_data.return_value = 1
+def test_humidity_is_not_up_to_date(sensor_hub, bus, device_address):
+    bus.read_byte_data.return_value = 1
 
     humidity = sensor_hub.get_humidity()
 
     assert humidity == -1
-    sensor_hub.bus.read_byte_data.assert_called_once_with(device_address, SensorRegister.ON_BOARD_SENSOR_OUT_OF_DATE.value)
+    bus.read_byte_data.assert_called_once_with(device_address, SensorRegister.ON_BOARD_SENSOR_OUT_OF_DATE.value)
 
 
-def test_humidity_returned_when_it_is_up_to_date(sensor_hub, device_address):
+def test_humidity_returned_when_it_is_up_to_date(sensor_hub, bus, device_address):
     expected_humidity = 33
-    sensor_hub.bus.read_byte_data.side_effect = [0, expected_humidity]
+    bus.read_byte_data.side_effect = [0, expected_humidity]
 
     humidity = sensor_hub.get_humidity()
 
     assert humidity == expected_humidity
-    sensor_hub.bus.read_byte_data.assert_has_calls([call(device_address, SensorRegister.ON_BOARD_SENSOR_OUT_OF_DATE.value),
-                                                      call(device_address, SensorRegister.ON_BOARD_HUMIDITY.value)])
+    bus.read_byte_data.assert_has_calls([
+        call(device_address, SensorRegister.ON_BOARD_SENSOR_OUT_OF_DATE.value),
+        call(device_address, SensorRegister.ON_BOARD_HUMIDITY.value)
+    ])
 
 
-def test_on_board_temperature_is_not_up_to_date(sensor_hub, device_address):
-    sensor_hub.bus.read_byte_data.return_value = 1
+def test_on_board_temperature_is_not_up_to_date(sensor_hub, bus, device_address):
+    bus.read_byte_data.return_value = 1
 
     temperature = sensor_hub.get_temperature()
 
     assert temperature == -1
-    sensor_hub.bus.read_byte_data.assert_called_once_with(device_address, SensorRegister.ON_BOARD_SENSOR_OUT_OF_DATE.value)
+    bus.read_byte_data.assert_called_once_with(
+        device_address, SensorRegister.ON_BOARD_SENSOR_OUT_OF_DATE.value
+    )
 
 
-def test_on_board_temperature_returned_when_it_is_up_to_date(sensor_hub, device_address):
+def test_on_board_temperature_returned_when_it_is_up_to_date(sensor_hub, bus, device_address):
     expected_temperature = 33
-    sensor_hub.bus.read_byte_data.side_effect = [0, expected_temperature]
+    bus.read_byte_data.side_effect = [0, expected_temperature]
 
     temperature = sensor_hub.get_temperature()
 
     assert temperature == expected_temperature
-    sensor_hub.bus.read_byte_data.assert_has_calls(
-        [call(device_address, SensorRegister.ON_BOARD_SENSOR_OUT_OF_DATE.value),
-         call(device_address, SensorRegister.ON_BOARD_TEMPERATURE.value)])
+    bus.read_byte_data.assert_has_calls([
+        call(device_address, SensorRegister.ON_BOARD_SENSOR_OUT_OF_DATE.value),
+        call(device_address, SensorRegister.ON_BOARD_TEMPERATURE.value)
+    ])
 
 
 @mark.parametrize("motion_detected, register_reading", [(True, 1), (False, 0)])
-def test_motion_detection(motion_detected, register_reading, sensor_hub, device_address):
-    sensor_hub.bus.read_byte_data.return_value = register_reading
+def test_motion_detection(motion_detected, register_reading, sensor_hub, bus, device_address):
+    bus.read_byte_data.return_value = register_reading
 
     assert sensor_hub.is_motion_detected() is motion_detected
 
-    sensor_hub.bus.read_byte_data.assert_called_once_with(device_address, SensorRegister.MOTION.value)
+    bus.read_byte_data.assert_called_once_with(device_address, SensorRegister.MOTION.value)
 
 
 @mark.parametrize("error_code", [
@@ -132,13 +131,13 @@ def test_motion_detection(motion_detected, register_reading, sensor_hub, device_
     0b1001,
     0b1011
 ])
-def test_brightness_sensor_error(error_code, sensor_hub, device_address):
-    sensor_hub.bus.read_byte_data.return_value = 1000
+def test_brightness_sensor_error(error_code, sensor_hub, bus, device_address):
+    bus.read_byte_data.return_value = 1000
 
     with raises(IOError, match="Error accessing light sensor"):
         sensor_hub.get_brightness()
 
-    sensor_hub.bus.read_byte_data.assert_called_once_with(device_address, SensorRegister.STATUS.value)
+    bus.read_byte_data.assert_called_once_with(device_address, SensorRegister.STATUS.value)
 
 
 @mark.parametrize("error_code", [
@@ -147,12 +146,12 @@ def test_brightness_sensor_error(error_code, sensor_hub, device_address):
     0b0111,
     0b0101
 ])
-def test_brightness_out_of_range_returns_minus_1(error_code, sensor_hub, device_address):
-    sensor_hub.bus.read_byte_data.return_value = 100
+def test_brightness_out_of_range_returns_minus_1(error_code, sensor_hub, bus, device_address):
+    bus.read_byte_data.return_value = 100
 
     brightness = sensor_hub.get_brightness()
 
-    sensor_hub.bus.read_byte_data.assert_called_once_with(device_address, SensorRegister.STATUS.value)
+    bus.read_byte_data.assert_called_once_with(device_address, SensorRegister.STATUS.value)
     assert brightness == -1
 
 
@@ -162,56 +161,56 @@ def test_brightness_out_of_range_returns_minus_1(error_code, sensor_hub, device_
     0b0010,     # temperature sensor error
     0b0011      # temperature out of range AND sensor error (just in case...)
 ])
-def test_brightness_is_returned(error_code, sensor_hub, device_address):
-    sensor_hub.bus.read_byte_data.side_effect = [error_code, 1, 39]
+def test_brightness_is_returned(error_code, sensor_hub, bus, device_address):
+    bus.read_byte_data.side_effect = [error_code, 1, 39]
 
     brightness = sensor_hub.get_brightness()
 
     assert brightness == 295
-    sensor_hub.bus.read_byte_data.assert_has_calls([
+    bus.read_byte_data.assert_has_calls([
         call(device_address, SensorRegister.LIGHT_HIGH.value),
         call(device_address, SensorRegister.LIGHT_LOW.value)
     ])
 
 
-def test_barometer_temperature_hardware_error(sensor_hub, device_address):
-    sensor_hub.bus.read_byte_data.return_value = 1
+def test_barometer_temperature_hardware_error(sensor_hub, bus, device_address):
+    bus.read_byte_data.return_value = 1
 
     with raises(IOError, match="Barometric Sensor Error"):
         sensor_hub.get_barometer_temperature()
 
-    sensor_hub.bus.read_byte_data.assert_called_once_with(device_address, SensorRegister.BAROMETRIC_SENSOR_STATUS.value)
+    bus.read_byte_data.assert_called_once_with(device_address, SensorRegister.BAROMETRIC_SENSOR_STATUS.value)
 
 
-def test_barometer_temperature_returns_correct_reading(sensor_hub, device_address):
+def test_barometer_temperature_returns_correct_reading(sensor_hub, bus, device_address):
     expected_temperature = 36
-    sensor_hub.bus.read_byte_data.side_effect = [0, expected_temperature]
+    bus.read_byte_data.side_effect = [0, expected_temperature]
 
     temperature = sensor_hub.get_barometer_temperature()
 
     assert temperature == expected_temperature
-    sensor_hub.bus.read_byte_data.assert_has_calls([
+    bus.read_byte_data.assert_has_calls([
         call(device_address, SensorRegister.BAROMETRIC_SENSOR_STATUS.value),
         call(device_address, SensorRegister.BAROMETRIC_TEMPERATURE.value)
     ])
 
 
-def test_barometer_pressure_hardware_error(sensor_hub, device_address):
-    sensor_hub.bus.read_byte_data.return_value = 1
+def test_barometer_pressure_hardware_error(sensor_hub, bus, device_address):
+    bus.read_byte_data.return_value = 1
 
     with raises(IOError, match="Barometric Sensor Error"):
         sensor_hub.get_barometer_pressure()
 
-    sensor_hub.bus.read_byte_data.assert_called_once_with(device_address, SensorRegister.BAROMETRIC_SENSOR_STATUS.value)
+    bus.read_byte_data.assert_called_once_with(device_address, SensorRegister.BAROMETRIC_SENSOR_STATUS.value)
 
 
-def test_barometer_pressure_returns_expected_reading(sensor_hub, device_address):
-    sensor_hub.bus.read_byte_data.side_effect = [0, 3, 5, 45]
+def test_barometer_pressure_returns_expected_reading(sensor_hub, bus, device_address):
+    bus.read_byte_data.side_effect = [0, 3, 5, 45]
 
     pressure = sensor_hub.get_barometer_pressure()
 
     assert pressure == 29504.03
-    sensor_hub.bus.read_byte_data.assert_has_calls([
+    bus.read_byte_data.assert_has_calls([
         call(device_address, SensorRegister.BAROMETRIC_SENSOR_STATUS.value),
         call(device_address, SensorRegister.BAROMETRIC_PRESSURE_LOW.value),
         call(device_address, SensorRegister.BAROMETRIC_PRESSURE_MIDDLE.value),