Skip to content

Commit 3bbfc0f

Browse files
committed
Stop using the mock_api for testing
The `mock_api` is using a real gRPC server, which is complicated and flaky. Instead of using a full blown gRPC server we now use mocks to mock the client directly. In the future, the microgrid client will provide a fake client to make it much more easy for users to test their code. This commit focuses on removing the use of `mock_api` preserving the test coverage as much as possible, and *not* on having a nice fake microgrid client in the SDK. This will come in the future. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent c4831b7 commit 3bbfc0f

File tree

2 files changed

+285
-92
lines changed

2 files changed

+285
-92
lines changed

tests/actor/test_data_sourcing.py

Lines changed: 241 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,92 +3,285 @@
33

44
"""Tests for the DataSourcingActor."""
55

6+
import asyncio
7+
from collections.abc import AsyncIterator, Callable
8+
from datetime import datetime, timezone
9+
from typing import TypeVar
10+
from unittest import mock
11+
12+
import pytest
13+
import pytest_mock
614
from frequenz.channels import Broadcast
7-
from frequenz.client.microgrid import ComponentCategory, ComponentMetricId
15+
from frequenz.client.microgrid import (
16+
BatteryComponentState,
17+
BatteryData,
18+
BatteryRelayState,
19+
Component,
20+
ComponentCategory,
21+
ComponentData,
22+
ComponentMetricId,
23+
EVChargerCableState,
24+
EVChargerComponentState,
25+
EVChargerData,
26+
InverterComponentState,
27+
InverterData,
28+
MeterData,
29+
)
830

931
from frequenz.sdk.actor import (
1032
ChannelRegistry,
1133
ComponentMetricRequest,
1234
DataSourcingActor,
1335
)
14-
from frequenz.sdk.microgrid import connection_manager
1536
from frequenz.sdk.timeseries import Quantity, Sample
16-
from tests.microgrid import mock_api
17-
18-
# pylint: disable=no-member
1937

38+
T = TypeVar("T", bound=ComponentData)
2039

21-
async def test_data_sourcing_actor() -> None:
22-
"""Tests for the DataSourcingActor."""
23-
servicer = mock_api.MockMicrogridServicer()
24-
server = mock_api.MockGrpcServer(servicer, port=57899)
25-
await server.start()
26-
27-
servicer.add_component(1, ComponentCategory.GRID)
28-
servicer.add_component(4, ComponentCategory.METER)
29-
servicer.add_component(7, ComponentCategory.METER)
30-
servicer.add_component(8, ComponentCategory.INVERTER)
31-
servicer.add_component(9, ComponentCategory.BATTERY)
3240

33-
servicer.add_connection(1, 4)
34-
servicer.add_connection(1, 7)
35-
servicer.add_connection(7, 8)
36-
servicer.add_connection(8, 9)
41+
@pytest.fixture
42+
def mock_connection_manager(mocker: pytest_mock.MockFixture) -> mock.Mock:
43+
"""Fixture for getting a mock connection manager."""
44+
mock_client = mock.MagicMock(name="connection_manager.get().api_client")
45+
mock_client.components = mock.AsyncMock(
46+
name="components()",
47+
return_value=[
48+
Component(component_id=4, category=ComponentCategory.METER),
49+
Component(component_id=6, category=ComponentCategory.INVERTER),
50+
Component(component_id=9, category=ComponentCategory.BATTERY),
51+
Component(component_id=12, category=ComponentCategory.EV_CHARGER),
52+
],
53+
)
54+
mock_client.meter_data = _new_meter_data_mock(4, starting_value=100.0)
55+
mock_client.inverter_data = _new_inverter_data_mock(6, starting_value=0.0)
56+
mock_client.battery_data = _new_battery_data_mock(9, starting_value=9.0)
57+
mock_client.ev_charger_data = _new_ev_charger_data_mock(12, starting_value=-13.0)
58+
mock_conn_manager = mock.MagicMock(name="connection_manager")
59+
mocker.patch(
60+
"frequenz.sdk.actor._data_sourcing.microgrid_api_source.connection_manager.get",
61+
return_value=mock_conn_manager,
62+
)
63+
mock_conn_manager.api_client = mock_client
64+
return mock_conn_manager
3765

38-
await connection_manager.initialize("grpc://[::1]:57899")
3966

67+
async def test_data_sourcing_actor( # pylint: disable=too-many-locals
68+
mock_connection_manager: mock.Mock, # pylint: disable=redefined-outer-name,unused-argument
69+
) -> None:
70+
"""Tests for the DataSourcingActor."""
4071
req_chan = Broadcast[ComponentMetricRequest](name="data_sourcing_requests")
4172
req_sender = req_chan.new_sender()
4273

4374
registry = ChannelRegistry(name="test-registry")
4475

4576
async with DataSourcingActor(req_chan.new_receiver(), registry):
46-
active_power_request = ComponentMetricRequest(
77+
active_power_request_4 = ComponentMetricRequest(
4778
"test-namespace", 4, ComponentMetricId.ACTIVE_POWER, None
4879
)
49-
active_power_recv = registry.get_or_create(
50-
Sample[Quantity], active_power_request.get_channel_name()
80+
active_power_recv_4 = registry.get_or_create(
81+
Sample[Quantity], active_power_request_4.get_channel_name()
5182
).new_receiver()
52-
await req_sender.send(active_power_request)
83+
await req_sender.send(active_power_request_4)
5384

54-
reactive_power_request = ComponentMetricRequest(
85+
reactive_power_request_4 = ComponentMetricRequest(
5586
"test-namespace", 4, ComponentMetricId.REACTIVE_POWER, None
5687
)
57-
_ = registry.get_or_create(
58-
Sample[Quantity], reactive_power_request.get_channel_name()
88+
reactive_power_recv_4 = registry.get_or_create(
89+
Sample[Quantity], reactive_power_request_4.get_channel_name()
5990
).new_receiver()
60-
await req_sender.send(reactive_power_request)
91+
await req_sender.send(reactive_power_request_4)
6192

62-
soc_request = ComponentMetricRequest(
93+
active_power_request_6 = ComponentMetricRequest(
94+
"test-namespace", 6, ComponentMetricId.ACTIVE_POWER, None
95+
)
96+
active_power_recv_6 = registry.get_or_create(
97+
Sample[Quantity], active_power_request_6.get_channel_name()
98+
).new_receiver()
99+
await req_sender.send(active_power_request_6)
100+
101+
soc_request_9 = ComponentMetricRequest(
63102
"test-namespace", 9, ComponentMetricId.SOC, None
64103
)
65-
soc_recv = registry.get_or_create(
66-
Sample[Quantity], soc_request.get_channel_name()
104+
soc_recv_9 = registry.get_or_create(
105+
Sample[Quantity], soc_request_9.get_channel_name()
67106
).new_receiver()
68-
await req_sender.send(soc_request)
107+
await req_sender.send(soc_request_9)
69108

70-
soc2_request = ComponentMetricRequest(
109+
soc2_request_9 = ComponentMetricRequest(
71110
"test-namespace", 9, ComponentMetricId.SOC, None
72111
)
73-
soc2_recv = registry.get_or_create(
74-
Sample[Quantity], soc2_request.get_channel_name()
112+
soc2_recv_9 = registry.get_or_create(
113+
Sample[Quantity], soc2_request_9.get_channel_name()
75114
).new_receiver()
76-
await req_sender.send(soc2_request)
115+
await req_sender.send(soc2_request_9)
77116

78-
for _ in range(3):
79-
sample = await soc_recv.receive()
117+
active_power_request_12 = ComponentMetricRequest(
118+
"test-namespace", 12, ComponentMetricId.ACTIVE_POWER, None
119+
)
120+
active_power_recv_12 = registry.get_or_create(
121+
Sample[Quantity], active_power_request_12.get_channel_name()
122+
).new_receiver()
123+
await req_sender.send(active_power_request_12)
124+
125+
for i in range(3):
126+
sample = await active_power_recv_4.receive()
80127
assert sample.value is not None
81-
assert 9.0 == sample.value.base_value
128+
assert 100.0 + i == sample.value.base_value
82129

83-
sample = await soc2_recv.receive()
130+
sample = await reactive_power_recv_4.receive()
84131
assert sample.value is not None
85-
assert 9.0 == sample.value.base_value
132+
assert 100.0 + i == sample.value.base_value
86133

87-
sample = await active_power_recv.receive()
134+
sample = await active_power_recv_6.receive()
88135
assert sample.value is not None
89-
assert 100.0 == sample.value.base_value
136+
assert 0.0 + i == sample.value.base_value
90137

91-
assert await server.graceful_shutdown()
92-
connection_manager._CONNECTION_MANAGER = ( # pylint: disable=protected-access
93-
None
94-
)
138+
sample = await soc_recv_9.receive()
139+
assert sample.value is not None
140+
assert 9.0 + i == sample.value.base_value
141+
142+
sample = await soc2_recv_9.receive()
143+
assert sample.value is not None
144+
assert 9.0 + i == sample.value.base_value
145+
146+
sample = await active_power_recv_12.receive()
147+
assert sample.value is not None
148+
assert -13.0 + i == sample.value.base_value
149+
150+
151+
def _new_meter_data(component_id: int, timestamp: datetime, value: float) -> MeterData:
152+
return MeterData(
153+
component_id=component_id,
154+
timestamp=timestamp,
155+
active_power=value,
156+
active_power_per_phase=(value, value, value),
157+
current_per_phase=(value, value, value),
158+
frequency=value,
159+
reactive_power=value,
160+
reactive_power_per_phase=(value, value, value),
161+
voltage_per_phase=(value, value, value),
162+
)
163+
164+
165+
def _new_inverter_data(
166+
component_id: int, timestamp: datetime, value: float
167+
) -> InverterData:
168+
return InverterData(
169+
component_id=component_id,
170+
timestamp=timestamp,
171+
active_power=value,
172+
reactive_power=value,
173+
frequency=value,
174+
reactive_power_per_phase=(value, value, value),
175+
active_power_per_phase=(value, value, value),
176+
current_per_phase=(value, value, value),
177+
voltage_per_phase=(value, value, value),
178+
active_power_exclusion_lower_bound=value,
179+
active_power_exclusion_upper_bound=value,
180+
active_power_inclusion_lower_bound=value,
181+
active_power_inclusion_upper_bound=value,
182+
component_state=InverterComponentState.UNSPECIFIED,
183+
errors=[],
184+
)
185+
186+
187+
def _new_battery_data(
188+
component_id: int, timestamp: datetime, value: float
189+
) -> BatteryData:
190+
return BatteryData(
191+
component_id=component_id,
192+
timestamp=timestamp,
193+
soc=value,
194+
temperature=value,
195+
component_state=BatteryComponentState.UNSPECIFIED,
196+
errors=[],
197+
soc_lower_bound=value,
198+
soc_upper_bound=value,
199+
capacity=value,
200+
power_exclusion_lower_bound=value,
201+
power_exclusion_upper_bound=value,
202+
power_inclusion_lower_bound=value,
203+
power_inclusion_upper_bound=value,
204+
relay_state=BatteryRelayState.UNSPECIFIED,
205+
)
206+
207+
208+
def _new_ev_charger_data(
209+
component_id: int, timestamp: datetime, value: float
210+
) -> EVChargerData:
211+
return EVChargerData(
212+
component_id=component_id,
213+
timestamp=timestamp,
214+
active_power=value,
215+
active_power_per_phase=(value, value, value),
216+
current_per_phase=(value, value, value),
217+
frequency=value,
218+
reactive_power=value,
219+
reactive_power_per_phase=(value, value, value),
220+
voltage_per_phase=(value, value, value),
221+
active_power_exclusion_lower_bound=value,
222+
active_power_exclusion_upper_bound=value,
223+
active_power_inclusion_lower_bound=value,
224+
active_power_inclusion_upper_bound=value,
225+
cable_state=EVChargerCableState.UNSPECIFIED,
226+
component_state=EVChargerComponentState.UNSPECIFIED,
227+
)
228+
229+
230+
def _new_streamer_mock(
231+
name: str,
232+
constructor: Callable[[int, datetime, float], T],
233+
component_id: int,
234+
starting_value: float,
235+
) -> mock.AsyncMock:
236+
"""Get a mock streamer."""
237+
238+
async def generate_data(starting_value: float) -> AsyncIterator[T]:
239+
value = starting_value
240+
while True:
241+
yield constructor(component_id, datetime.now(timezone.utc), value)
242+
await asyncio.sleep(0) # Let other tasks run
243+
value += 1.0
244+
245+
return mock.AsyncMock(name=name, return_value=generate_data(starting_value))
246+
247+
248+
def _new_meter_data_mock(component_id: int, starting_value: float) -> mock.AsyncMock:
249+
"""Get a mock streamer for meter data."""
250+
return _new_streamer_mock(
251+
f"meter_data_mock(id={component_id}, starting_value={starting_value})",
252+
_new_meter_data,
253+
component_id,
254+
starting_value,
255+
)
256+
257+
258+
def _new_inverter_data_mock(component_id: int, starting_value: float) -> mock.AsyncMock:
259+
"""Get a mock streamer for inverter data."""
260+
return _new_streamer_mock(
261+
f"inverter_data_mock(id={component_id}, starting_value={starting_value})",
262+
_new_inverter_data,
263+
component_id,
264+
starting_value,
265+
)
266+
267+
268+
def _new_battery_data_mock(component_id: int, starting_value: float) -> mock.AsyncMock:
269+
"""Get a mock streamer for battery data."""
270+
return _new_streamer_mock(
271+
f"battery_data_mock(id={component_id}, starting_value={starting_value})",
272+
_new_battery_data,
273+
component_id,
274+
starting_value,
275+
)
276+
277+
278+
def _new_ev_charger_data_mock(
279+
component_id: int, starting_value: float
280+
) -> mock.AsyncMock:
281+
"""Get a mock streamer for EV charger data."""
282+
return _new_streamer_mock(
283+
f"ev_charger_data_mock(id={component_id}, starting_value={starting_value})",
284+
_new_ev_charger_data,
285+
component_id,
286+
starting_value,
287+
)

0 commit comments

Comments
 (0)