Skip to content

Commit c28e03a

Browse files
authored
Merge pull request #165 from jbergler/refactor-test-infra
update test infra to make testing different combinations of features easier
2 parents c869a1e + 8e86c99 commit c28e03a

6 files changed

+111
-37
lines changed

tests/conftest.py

+75-19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Global fixtures for ttlock integration."""
22

33
from time import time
4+
from typing import NamedTuple
45
from unittest.mock import patch
56

67
from aiohttp import ClientSession
@@ -18,7 +19,12 @@
1819
from homeassistant.core import HomeAssistant
1920
from homeassistant.setup import async_setup_component
2021

21-
from .const import LOCK_DETAILS, LOCK_STATE_UNLOCKED, PASSAGE_MODE_6_TO_6_7_DAYS
22+
from .const import (
23+
BASIC_LOCK_DETAILS,
24+
LOCK_STATE_LOCKED,
25+
LOCK_STATE_UNLOCKED,
26+
PASSAGE_MODE_6_TO_6_7_DAYS,
27+
)
2228

2329
pytest_plugins = "pytest_homeassistant_custom_component"
2430

@@ -93,22 +99,72 @@ async def coordinator(hass, api):
9399
return LockUpdateCoordinator(hass, api, 7252408)
94100

95101

102+
class MockApiData(NamedTuple):
103+
"""Container for mock API response data."""
104+
105+
lock: Lock
106+
state: LockState
107+
passage_mode: PassageModeConfig | None
108+
109+
96110
@pytest.fixture
97-
def sane_default_data():
98-
"""Fixture for mocking sane default data from the API."""
99-
100-
lock = Lock.parse_obj(LOCK_DETAILS)
101-
state = LockState.parse_obj(LOCK_STATE_UNLOCKED)
102-
passage_mode_config = PassageModeConfig.parse_obj(PASSAGE_MODE_6_TO_6_7_DAYS)
103-
104-
with patch(
105-
"custom_components.ttlock.api.TTLockApi.get_locks", return_value=[lock.id]
106-
), patch(
107-
"custom_components.ttlock.api.TTLockApi.get_lock", return_value=lock
108-
), patch(
109-
"custom_components.ttlock.api.TTLockApi.get_lock_state", return_value=state
110-
), patch(
111-
"custom_components.ttlock.api.TTLockApi.get_lock_passage_mode_config",
112-
return_value=passage_mode_config,
113-
):
114-
yield
111+
def mock_data_factory():
112+
"""Factory fixture to create different sets of mock data."""
113+
114+
def create_mock_data(scenario: str = "default") -> MockApiData:
115+
scenarios = {
116+
"default": MockApiData(
117+
lock=Lock.parse_obj(BASIC_LOCK_DETAILS),
118+
state=LockState.parse_obj(LOCK_STATE_UNLOCKED),
119+
passage_mode=PassageModeConfig.parse_obj(PASSAGE_MODE_6_TO_6_7_DAYS),
120+
),
121+
"locked": MockApiData(
122+
lock=Lock.parse_obj(BASIC_LOCK_DETAILS),
123+
state=LockState.parse_obj(LOCK_STATE_LOCKED),
124+
passage_mode=PassageModeConfig.parse_obj(PASSAGE_MODE_6_TO_6_7_DAYS),
125+
),
126+
"no_passage_mode": MockApiData(
127+
lock=Lock.parse_obj(BASIC_LOCK_DETAILS),
128+
state=LockState.parse_obj(LOCK_STATE_UNLOCKED),
129+
passage_mode=None,
130+
),
131+
}
132+
return scenarios[scenario]
133+
134+
return create_mock_data
135+
136+
137+
@pytest.fixture
138+
def mock_api_responses(monkeypatch, mock_data_factory):
139+
"""Fixture for mocking TTLock API responses with configurable data."""
140+
141+
def create_mock_responses(scenario: str = "default"):
142+
mock_data = mock_data_factory(scenario)
143+
144+
async def mock_get_locks(*args, **kwargs):
145+
return [mock_data.lock.id]
146+
147+
async def mock_get_lock(*args, **kwargs):
148+
return mock_data.lock
149+
150+
async def mock_get_lock_state(*args, **kwargs):
151+
return mock_data.state
152+
153+
async def mock_get_passage_mode(*args, **kwargs):
154+
return mock_data.passage_mode
155+
156+
monkeypatch.setattr(
157+
"custom_components.ttlock.api.TTLockApi.get_locks", mock_get_locks
158+
)
159+
monkeypatch.setattr(
160+
"custom_components.ttlock.api.TTLockApi.get_lock", mock_get_lock
161+
)
162+
monkeypatch.setattr(
163+
"custom_components.ttlock.api.TTLockApi.get_lock_state", mock_get_lock_state
164+
)
165+
monkeypatch.setattr(
166+
"custom_components.ttlock.api.TTLockApi.get_lock_passage_mode_config",
167+
mock_get_passage_mode,
168+
)
169+
170+
return create_mock_responses

tests/const.py

+8-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Dummy data for tests."""
22

3-
LOCK_DETAILS = {
3+
BASIC_LOCK_DETAILS = {
44
"date": 1669690212000,
55
"lockAlias": "Front Door",
66
"lockSound": 2,
@@ -44,8 +44,9 @@
4444
"sensitivity": -1,
4545
}
4646

47-
LOCK_STATE_LOCKED = {"state": 0}
48-
LOCK_STATE_UNLOCKED = {"state": 1}
47+
LOCK_STATE_LOCKED = {"state": 0, "electricQuantity": 90, "lockTime": 1621459200000}
48+
49+
LOCK_STATE_UNLOCKED = {"state": 1, "electricQuantity": 90, "lockTime": 1621459200000}
4950

5051
PASSAGE_MODE_6_TO_6_7_DAYS = {
5152
"autoUnlock": 2,
@@ -64,8 +65,8 @@
6465
}
6566

6667
WEBHOOK_LOCK_10AM_UTC = {
67-
"lockId": LOCK_DETAILS["lockId"],
68-
"lockMac": LOCK_DETAILS["lockMac"],
68+
"lockId": BASIC_LOCK_DETAILS["lockId"],
69+
"lockMac": BASIC_LOCK_DETAILS["lockMac"],
6970
"electricQuantity": 40,
7071
"serverDate": 1682244497000,
7172
"lockDate": 1682244497000,
@@ -75,8 +76,8 @@
7576
}
7677

7778
WEBHOOK_UNLOCK_10AM_UTC = {
78-
"lockId": LOCK_DETAILS["lockId"],
79-
"lockMac": LOCK_DETAILS["lockMac"],
79+
"lockId": BASIC_LOCK_DETAILS["lockId"],
80+
"lockMac": BASIC_LOCK_DETAILS["lockMac"],
8081
"electricQuantity": 40,
8182
"serverDate": 1682244497000,
8283
"lockDate": 1682244497000,

tests/test_coordinator.py

+10-6
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from custom_components.ttlock.models import PassageModeConfig, WebhookEvent
1111

1212
from .const import (
13-
LOCK_DETAILS,
13+
BASIC_LOCK_DETAILS,
1414
PASSAGE_MODE_6_TO_6_7_DAYS,
1515
PASSAGE_MODE_ALL_DAY_WEEKDAYS,
1616
WEBHOOK_LOCK_10AM_UTC,
@@ -19,11 +19,12 @@
1919

2020

2121
async def test_coordinator_loads_data(
22-
coordinator: LockUpdateCoordinator, sane_default_data
22+
coordinator: LockUpdateCoordinator, mock_api_responses
2323
):
24+
mock_api_responses("default")
2425
await coordinator.async_refresh()
2526

26-
assert coordinator.data.name == LOCK_DETAILS["lockAlias"]
27+
assert coordinator.data.name == BASIC_LOCK_DETAILS["lockAlias"]
2728
assert coordinator.data.locked is False
2829
assert coordinator.data.action_pending is False
2930
assert coordinator.data.last_user is None
@@ -132,8 +133,9 @@ def test_is_none_when_passage_mode_is_all_day(self, lock_state, time):
132133
class TestLockUpdateCoordinator:
133134
class TestProcessWebhookData:
134135
async def test_lock_works(
135-
self, coordinator: LockUpdateCoordinator, sane_default_data
136+
self, coordinator: LockUpdateCoordinator, mock_api_responses
136137
):
138+
mock_api_responses("default")
137139
await coordinator.async_refresh()
138140
coordinator.data.locked = False
139141

@@ -146,8 +148,9 @@ async def test_lock_works(
146148
assert coordinator.data.last_reason == "lock by lock key"
147149

148150
async def test_unlock_works(
149-
self, coordinator: LockUpdateCoordinator, sane_default_data
151+
self, coordinator: LockUpdateCoordinator, mock_api_responses
150152
):
153+
mock_api_responses("default")
151154
await coordinator.async_refresh()
152155
coordinator.data.locked = True
153156
coordinator.data.auto_lock_seconds = -1
@@ -160,8 +163,9 @@ async def test_unlock_works(
160163
assert coordinator.data.last_reason == "unlock by IC card"
161164

162165
async def test_auto_lock_works(
163-
self, hass, coordinator: LockUpdateCoordinator, sane_default_data
166+
self, hass, coordinator: LockUpdateCoordinator, mock_api_responses
164167
):
168+
mock_api_responses("default")
165169
await coordinator.async_refresh()
166170
coordinator.data.locked = True
167171
coordinator.data.auto_lock_seconds = 1

tests/test_init.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
from homeassistant.helpers.network import NoURLAvailableError
88

99

10-
async def test_setup_unload_and_reload_entry(hass, component_setup, sane_default_data):
10+
async def test_setup_unload_and_reload_entry(hass, component_setup, mock_api_responses):
1111
"""Test entry setup and unload."""
12+
mock_api_responses("default")
1213
await component_setup()
1314

1415
entries = hass.config_entries.async_entries(DOMAIN)
@@ -24,7 +25,8 @@ async def test_setup_unload_and_reload_entry(hass, component_setup, sane_default
2425
"homeassistant.components.webhook.async_generate_url",
2526
side_effect=NoURLAvailableError,
2627
)
27-
async def test_no_url(hass, component_setup, sane_default_data):
28+
async def test_no_url(hass, component_setup, mock_api_responses):
29+
mock_api_responses("default")
2830
with patch("homeassistant.helpers.issue_registry.async_create_issue") as mock:
2931
assert await component_setup()
3032
assert mock.assert_called

tests/test_lock.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from homeassistant.core import HomeAssistant
2+
3+
4+
async def test_lock_states(hass: HomeAssistant, mock_api_responses, component_setup):
5+
"""Test different lock states."""
6+
mock_api_responses("default")
7+
coordinator = await component_setup()
8+
assert not coordinator.data.locked

tests/test_services.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616

1717
class Test_create_passcode:
1818
async def test_can_create_passcode(
19-
self, hass: HomeAssistant, component_setup, sane_default_data
19+
self, hass: HomeAssistant, component_setup, mock_api_responses
2020
):
21+
mock_api_responses("default")
2122
coordinator = await component_setup()
2223

2324
attrs = {
@@ -54,9 +55,10 @@ async def test_can_create_passcode(
5455
class Test_cleanup_passcodes:
5556
@pytest.mark.parametrize("return_response", (True, False))
5657
async def test_works_when_there_is_nothing_to_do(
57-
self, hass: HomeAssistant, component_setup, sane_default_data, return_response
58+
self, hass: HomeAssistant, component_setup, mock_api_responses, return_response
5859
) -> None:
5960
"""Test get schedule service."""
61+
mock_api_responses("default")
6062
coordinator = await component_setup()
6163

6264
with patch(
@@ -81,9 +83,10 @@ async def test_works_when_there_is_an_expired_passcode(
8183
self,
8284
hass: HomeAssistant,
8385
component_setup,
84-
sane_default_data,
86+
mock_api_responses,
8587
) -> None:
8688
"""Test get schedule service."""
89+
mock_api_responses("default")
8790
coordinator = await component_setup()
8891

8992
with patch(

0 commit comments

Comments
 (0)