如何对使用python-can模块的代码进行单元测试,规避非目标环境CAN总线初始化报错
问题原因
- 你当前的patch不生效核心原因是:
UsingCanBUS类的静态属性_bus会在模块导入阶段直接执行can.ThreadSafeBus初始化,而你现有测试的patch时机晚于模块导入,真实的CAN接口已经被调用抛出异常,mock没有来得及替换目标对象。 - 另外如果测试运行时
mymodule已经被提前导入过,后续的patch也无法覆盖已经加载的模块内容。
解决方案1:调整patch时机(无需修改业务代码)
在测试会话启动、任何业务代码导入之前,就完成对mymodule.can的全局mock,使用pytest的自动fixture实现最方便:
- 在
tests目录下新建conftest.py,写入全局mock逻辑:
# tests/conftest.py import pytest from unittest.mock import MagicMock, patch @pytest.fixture(scope="session", autouse=True) def mock_can_module(): # 创建mock的can模块对象 mock_can = MagicMock() # 预先设置需要用到的属性和返回值 mock_can.CanError = type("CanError", (Exception,), {}) mock_bus = MagicMock() mock_can.ThreadSafeBus.return_value = mock_bus # 全局替换mymodule中导入的can模块,整个测试周期生效 with patch("mymodule.can", mock_can): yield mock_can, mock_bus
- 测试函数直接导入业务代码即可,此时所有CAN相关调用都会走mock对象:
#./tests/test_utils/test_utils.py def test_something(mock_can_module): mock_can, mock_bus = mock_can_module from mymodule.utils import some_function # 可自定义mock的返回值/异常,验证逻辑正确性 mock_bus.send.side_effect = mock_can.CanError assert some_function() == expected_result
解决方案2:优化业务代码(更推荐,长期可维护性更高)
把静态总线的初始化改为懒加载,仅在第一次实例化类的时候才创建总线对象,避免导入阶段就调用硬件接口:
# ./mymodule/__init__.py import can from typing import Optional class UsingCanBUS: _bus: Optional[can.ThreadSafeBus] = None def __new__(cls, *args, **kwargs): # 第一次创建实例时才初始化总线 if cls._bus is None: cls._bus = can.ThreadSafeBus(channel='can0', bustype='socketcan') return super().__new__(cls, *args, **kwargs) def __init__(self) -> None: # 原有逻辑不变 self._listener = can.Listener() self._listener.on_message_received = self._handle_message def _send_message(self, id, data) -> bool: msg = can.Message(arbitration_id=id, data=data, extended_id=False) try: self._bus.send(msg) except can.CanError: return False else: return True
改完之后测试时不需要处理导入时机问题,只需要在创建UsingCanBUS实例之前完成patch即可,也方便后续生产环境动态修改总线参数。
内容的提问来源于stack exchange,提问作者powerbling
相关产品推荐
相关产品推荐

