fix: unit test refactoring with conftest and more mocks
ref: N25B-205
This commit is contained in:
@@ -22,6 +22,6 @@ test:
|
||||
tags:
|
||||
- test
|
||||
script:
|
||||
- uv run --only-group test pytest test/integration
|
||||
- uv run --only-group integration-test pytest test/integration
|
||||
- uv run --only-group test pytest test/unit
|
||||
|
||||
|
||||
@@ -29,6 +29,12 @@ dev = [
|
||||
"ruff>=0.14.2",
|
||||
"ruff-format>=0.3.0",
|
||||
]
|
||||
integration-test = [
|
||||
{include-group = "test"},
|
||||
"asyncio>=4.0.0",
|
||||
"soundfile>=0.13.1",
|
||||
"zmq>=0.0.0",
|
||||
]
|
||||
test = [
|
||||
"pytest>=8.4.2",
|
||||
"pytest-asyncio>=1.2.0",
|
||||
|
||||
@@ -7,7 +7,6 @@ import zmq
|
||||
|
||||
from control_backend.core.config import settings
|
||||
from control_backend.core.zmq_context import context
|
||||
from control_backend.schemas.message import Message
|
||||
from control_backend.agents.ri_command_agent import RICommandAgent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import asyncio
|
||||
import zmq
|
||||
import json
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from control_backend.agents.ri_command_agent import RICommandAgent
|
||||
from control_backend.schemas.ri_message import SpeechCommand
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -81,47 +81,45 @@ def fake_json_invalid_id_negototiate():
|
||||
}
|
||||
)
|
||||
|
||||
def mock_command_agent():
|
||||
"""Fixture to create a mock BDIAgent."""
|
||||
agent = MagicMock()
|
||||
agent.bdi = MagicMock()
|
||||
agent.jid = "ri_command_agent@test"
|
||||
return agent
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_setup_creates_socket_and_negotiate_1(monkeypatch):
|
||||
"""
|
||||
Test the setup of the communication agent
|
||||
"""
|
||||
# --- Arrange ---
|
||||
fake_socket = MagicMock()
|
||||
fake_socket.send_json = AsyncMock()
|
||||
fake_socket.recv_json = fake_json_correct_negototiate_1()
|
||||
fake_socket.recv_json = AsyncMock(return_value={
|
||||
"endpoint": "negotiate/ports",
|
||||
"data": [
|
||||
{"id": "main", "port": 5555, "bind": False},
|
||||
{"id": "actuation", "port": 5556, "bind": True},
|
||||
],
|
||||
})
|
||||
|
||||
# Mock context.socket to return our fake socket
|
||||
monkeypatch.setattr(
|
||||
"control_backend.agents.ri_communication_agent.context.socket", lambda _: fake_socket
|
||||
)
|
||||
|
||||
# Mock RICommandAgent agent startup
|
||||
with patch(
|
||||
"control_backend.agents.ri_communication_agent.RICommandAgent", autospec=True
|
||||
) as MockCommandAgent:
|
||||
with patch("control_backend.agents.ri_communication_agent.RICommandAgent") as MockCommandAgent:
|
||||
fake_agent_instance = MockCommandAgent.return_value
|
||||
fake_agent_instance.start = AsyncMock()
|
||||
|
||||
# --- Act ---
|
||||
agent = RICommunicationAgent(
|
||||
"test@server", "password", address="tcp://localhost:5555", bind=False
|
||||
)
|
||||
agent = RICommunicationAgent("test@server", "password", address="tcp://localhost:5555", bind=False)
|
||||
await agent.setup()
|
||||
|
||||
# --- Assert ---
|
||||
fake_socket.connect.assert_any_call("tcp://localhost:5555")
|
||||
fake_socket.send_json.assert_any_call({"endpoint": "negotiate/ports", "data": None})
|
||||
fake_socket.recv_json.assert_awaited()
|
||||
fake_agent_instance.start.assert_awaited()
|
||||
MockCommandAgent.assert_called_once_with(
|
||||
ANY, # Server Name
|
||||
ANY, # Server Password
|
||||
address="tcp://*:5556", # derived from the 'port' value in negotiation
|
||||
bind=True,
|
||||
ANY, ANY, address="tcp://*:5556", bind=True
|
||||
)
|
||||
# Ensure the agent attached a ListenBehaviour
|
||||
assert any(isinstance(b, agent.ListenBehaviour) for b in agent.behaviours)
|
||||
|
||||
|
||||
@@ -141,6 +139,9 @@ async def test_setup_creates_socket_and_negotiate_2(monkeypatch):
|
||||
)
|
||||
|
||||
# Mock RICommandAgent agent startup
|
||||
|
||||
patch("control_backend.agents.ri_communication_agent.RICommandAgent", mock_command_agent)
|
||||
|
||||
with patch(
|
||||
"control_backend.agents.ri_communication_agent.RICommandAgent", autospec=True
|
||||
) as MockCommandAgent:
|
||||
|
||||
57
test/integration/conftest.py
Normal file
57
test/integration/conftest.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import sys
|
||||
from unittest.mock import MagicMock, AsyncMock
|
||||
|
||||
class DummyCyclicBehaviour:
|
||||
async def run(self):
|
||||
pass
|
||||
|
||||
def kill(self):
|
||||
self.is_killed = True
|
||||
return None
|
||||
|
||||
class DummyAgent:
|
||||
def __init__(self, jid=None, password=None, *_, **__):
|
||||
self.jid = jid
|
||||
self.password = password
|
||||
self.behaviours = []
|
||||
|
||||
async def start(self):
|
||||
return AsyncMock()
|
||||
|
||||
def add_behaviour(self, behaviour):
|
||||
behaviour.agent = self
|
||||
self.behaviours.append(behaviour)
|
||||
|
||||
async def stop(self):
|
||||
pass
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""
|
||||
This hook runs at the start of the pytest session, before any tests are
|
||||
collected. It mocks heavy or unavailable modules to prevent ImportErrors.
|
||||
"""
|
||||
# --- Mock spade and spade-bdi ---
|
||||
mock_spade = MagicMock()
|
||||
mock_spade.agent = MagicMock(Agent=DummyAgent)
|
||||
mock_spade.behaviour = MagicMock(CyclicBehaviour=DummyCyclicBehaviour)
|
||||
mock_spade_bdi = MagicMock()
|
||||
mock_spade_bdi.bdi = MagicMock()
|
||||
|
||||
mock_spade.agent.Message = MagicMock()
|
||||
|
||||
sys.modules["spade"] = mock_spade
|
||||
sys.modules["spade.agent"] = mock_spade.agent
|
||||
sys.modules["spade.behaviour"] = mock_spade.behaviour
|
||||
sys.modules["spade_bdi"] = mock_spade_bdi
|
||||
sys.modules["spade_bdi.bdi"] = mock_spade_bdi.bdi
|
||||
|
||||
# --- Mock the config module to prevent Pydantic ImportError ---
|
||||
mock_config_module = MagicMock()
|
||||
|
||||
# The code under test does `from ... import settings`, so our mock module
|
||||
# must have a `settings` attribute. We'll make it a MagicMock so we can
|
||||
# configure it later in our tests using mocker.patch.
|
||||
mock_config_module.settings = MagicMock()
|
||||
|
||||
sys.modules["control_backend.core.config"] = mock_config_module
|
||||
56
uv.lock
generated
56
uv.lock
generated
@@ -127,6 +127,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", size = 66419, upload-time = "2023-09-30T22:11:16.072Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asyncio"
|
||||
version = "4.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/ea/26c489a11f7ca862d5705db67683a7361ce11c23a7b98fc6c2deaeccede2/asyncio-4.0.0.tar.gz", hash = "sha256:570cd9e50db83bc1629152d4d0b7558d6451bb1bfd5dfc2e935d96fc2f40329b", size = 5371, upload-time = "2025-08-05T02:51:46.605Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/57/64/eff2564783bd650ca25e15938d1c5b459cda997574a510f7de69688cb0b4/asyncio-4.0.0-py3-none-any.whl", hash = "sha256:c1eddb0659231837046809e68103969b2bef8b0400d59cfa6363f6b5ed8cc88b", size = 5555, upload-time = "2025-08-05T02:51:45.767Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "25.4.0"
|
||||
@@ -1354,6 +1363,15 @@ dev = [
|
||||
{ name = "ruff" },
|
||||
{ name = "ruff-format" },
|
||||
]
|
||||
integration-test = [
|
||||
{ name = "asyncio" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "pytest-mock" },
|
||||
{ name = "soundfile" },
|
||||
{ name = "zmq" },
|
||||
]
|
||||
test = [
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
@@ -1387,6 +1405,15 @@ dev = [
|
||||
{ name = "ruff", specifier = ">=0.14.2" },
|
||||
{ name = "ruff-format", specifier = ">=0.3.0" },
|
||||
]
|
||||
integration-test = [
|
||||
{ name = "asyncio", specifier = ">=4.0.0" },
|
||||
{ name = "pytest", specifier = ">=8.4.2" },
|
||||
{ name = "pytest-asyncio", specifier = ">=1.2.0" },
|
||||
{ name = "pytest-cov", specifier = ">=7.0.0" },
|
||||
{ name = "pytest-mock", specifier = ">=3.15.1" },
|
||||
{ name = "soundfile", specifier = ">=0.13.1" },
|
||||
{ name = "zmq", specifier = ">=0.0.0" },
|
||||
]
|
||||
test = [
|
||||
{ name = "pytest", specifier = ">=8.4.2" },
|
||||
{ name = "pytest-asyncio", specifier = ">=1.2.0" },
|
||||
@@ -1412,7 +1439,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "4.3.0"
|
||||
@@ -2217,6 +2243,25 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soundfile"
|
||||
version = "0.13.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi" },
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/28/e2a36573ccbcf3d57c00626a21fe51989380636e821b341d36ccca0c1c3a/soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445", size = 25751, upload-time = "2025-01-25T09:16:44.235Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/ab/73e97a5b3cc46bba7ff8650a1504348fa1863a6f9d57d7001c6b67c5f20e/soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33", size = 1142250, upload-time = "2025-01-25T09:16:47.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/e5/58fd1a8d7b26fc113af244f966ee3aecf03cb9293cb935daaddc1e455e18/soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593", size = 1101406, upload-time = "2025-01-25T09:16:49.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/ae/c0e4a53d77cf6e9a04179535766b3321b0b9ced5f70522e4caf9329f0046/soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb", size = 1235729, upload-time = "2025-01-25T09:16:53.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/5e/70bdd9579b35003a489fc850b5047beeda26328053ebadc1fb60f320f7db/soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618", size = 1313646, upload-time = "2025-01-25T09:16:54.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/df/8c11dc4dfceda14e3003bb81a0d0edcaaf0796dd7b4f826ea3e532146bba/soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5", size = 899881, upload-time = "2025-01-25T09:16:56.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9", size = 1019162, upload-time = "2025-01-25T09:16:59.573Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spade"
|
||||
version = "4.1.0"
|
||||
@@ -2744,3 +2789,12 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmq"
|
||||
version = "0.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyzmq" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6e/78/833b2808793c1619835edb1a4e17a023d5d625f4f97ff25ffff986d1f472/zmq-0.0.0.tar.gz", hash = "sha256:6b1a1de53338646e8c8405803cffb659e8eb7bb02fff4c9be62a7acfac8370c9", size = 966, upload-time = "2015-05-21T17:34:26.603Z" }
|
||||
|
||||
Reference in New Issue
Block a user