test_mqtt_client.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. import pytest
  2. import sys
  3. import os
  4. from unittest.mock import MagicMock, patch
  5. sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  6. from modules.mqtt_client import MQTTClient
  7. def _setup_mock_client(mock_client_class):
  8. """创建 mock 客户端,并让 loop_start 触发 on_connect 回调"""
  9. mock_client = MagicMock()
  10. mock_client.connect.return_value = 0
  11. mock_client_class.return_value = mock_client
  12. # 捕获 on_connect 回调,在 loop_start 时触发它
  13. def fake_loop_start():
  14. import threading, time
  15. # 延迟调用以模拟异步回调,等待 connect() 完成 self.config 赋值
  16. def _trigger():
  17. time.sleep(0.05)
  18. mock_client.on_connect(mock_client, None, {}, 0)
  19. t = threading.Thread(target=_trigger, daemon=True)
  20. t.start()
  21. mock_client.loop_start.side_effect = fake_loop_start
  22. return mock_client
  23. class TestMQTTClient:
  24. def setup_method(self):
  25. self.mqtt_client = MQTTClient()
  26. self.data_callback = MagicMock()
  27. self.status_callback = MagicMock()
  28. self.mqtt_client.set_data_callback(self.data_callback)
  29. self.mqtt_client.set_status_callback(self.status_callback)
  30. def teardown_method(self):
  31. self.mqtt_client.disconnect()
  32. @patch('modules.mqtt_client.mqtt.Client')
  33. def test_connect_success(self, mock_client_class):
  34. """测试成功连接MQTT服务器"""
  35. mock_client = _setup_mock_client(mock_client_class)
  36. success, message = self.mqtt_client.connect(
  37. broker='localhost', port=1883, client_id='test_client', keepalive=60
  38. )
  39. assert success is True
  40. assert 'localhost' in message
  41. mock_client.connect.assert_called_once()
  42. mock_client.loop_start.assert_called_once()
  43. @patch('modules.mqtt_client.mqtt.Client')
  44. def test_connect_with_tls(self, mock_client_class):
  45. """测试使用TLS连接MQTT服务器"""
  46. mock_client = _setup_mock_client(mock_client_class)
  47. success, message = self.mqtt_client.connect(
  48. broker='localhost', port=8883, client_id='test_client',
  49. keepalive=60, use_tls=True, ca_certs='path/to/ca.crt'
  50. )
  51. assert success is True
  52. mock_client.tls_set.assert_called_once()
  53. @patch('modules.mqtt_client.mqtt.Client')
  54. def test_connect_with_auth(self, mock_client_class):
  55. """测试使用用户名密码连接MQTT服务器"""
  56. mock_client = _setup_mock_client(mock_client_class)
  57. success, message = self.mqtt_client.connect(
  58. broker='localhost', port=1883, client_id='test_client',
  59. keepalive=60, username='test_user', password='test_pass'
  60. )
  61. assert success is True
  62. mock_client.username_pw_set.assert_called_once_with('test_user', 'test_pass')
  63. @patch('modules.mqtt_client.mqtt.Client')
  64. def test_disconnect(self, mock_client_class):
  65. """测试断开MQTT连接"""
  66. mock_client = _setup_mock_client(mock_client_class)
  67. self.mqtt_client.connect(
  68. broker='localhost', port=1883, client_id='test_client', keepalive=60
  69. )
  70. self.mqtt_client.disconnect()
  71. mock_client.loop_stop.assert_called_once()
  72. mock_client.disconnect.assert_called_once()
  73. @patch('modules.mqtt_client.mqtt.Client')
  74. def test_publish_success(self, mock_client_class):
  75. """测试成功发布消息"""
  76. import paho.mqtt.client as paho
  77. mock_client = _setup_mock_client(mock_client_class)
  78. mock_publish = MagicMock()
  79. mock_publish.rc = paho.MQTT_ERR_SUCCESS
  80. mock_client.publish.return_value = mock_publish
  81. self.mqtt_client.connect(
  82. broker='localhost', port=1883, client_id='test_client', keepalive=60
  83. )
  84. success, message = self.mqtt_client.publish('test/topic', 'test message')
  85. assert success is True
  86. mock_client.publish.assert_called_once()
  87. @patch('modules.mqtt_client.mqtt.Client')
  88. def test_publish_not_connected(self, mock_client_class):
  89. """测试未连接时发布消息"""
  90. success, message = self.mqtt_client.publish('test/topic', 'test message')
  91. assert success is False
  92. assert '未连接' in message
  93. @patch('modules.mqtt_client.mqtt.Client')
  94. def test_subscribe_success(self, mock_client_class):
  95. """测试成功订阅主题"""
  96. import paho.mqtt.client as paho
  97. mock_client = _setup_mock_client(mock_client_class)
  98. mock_client.subscribe.return_value = (paho.MQTT_ERR_SUCCESS, 1)
  99. self.mqtt_client.connect(
  100. broker='localhost', port=1883, client_id='test_client', keepalive=60
  101. )
  102. success, message = self.mqtt_client.subscribe('test/topic')
  103. assert success is True
  104. mock_client.subscribe.assert_called_once()
  105. @patch('modules.mqtt_client.mqtt.Client')
  106. def test_subscribe_failure(self, mock_client_class):
  107. """测试订阅主题失败"""
  108. mock_client = _setup_mock_client(mock_client_class)
  109. mock_client.subscribe.return_value = (1, None)
  110. self.mqtt_client.connect(
  111. broker='localhost', port=1883, client_id='test_client', keepalive=60
  112. )
  113. success, message = self.mqtt_client.subscribe('test/topic')
  114. assert success is False
  115. @patch('modules.mqtt_client.mqtt.Client')
  116. def test_unsubscribe(self, mock_client_class):
  117. """测试取消订阅主题"""
  118. import paho.mqtt.client as paho
  119. mock_client = _setup_mock_client(mock_client_class)
  120. mock_unsub = MagicMock()
  121. mock_unsub.rc = paho.MQTT_ERR_SUCCESS
  122. mock_client.unsubscribe.return_value = mock_unsub
  123. self.mqtt_client.connect(
  124. broker='localhost', port=1883, client_id='test_client', keepalive=60
  125. )
  126. # subscribe first so topics list is populated
  127. mock_sub = MagicMock()
  128. mock_sub.rc = paho.MQTT_ERR_SUCCESS
  129. mock_client.subscribe.return_value = mock_sub
  130. self.mqtt_client.subscribe('test/topic')
  131. success, message = self.mqtt_client.unsubscribe('test/topic')
  132. assert success is True
  133. mock_client.unsubscribe.assert_called_once()
  134. @patch('modules.mqtt_client.mqtt.Client')
  135. def test_get_status(self, mock_client_class):
  136. """测试获取MQTT客户端状态"""
  137. mock_client = _setup_mock_client(mock_client_class)
  138. # 初始状态
  139. st = self.mqtt_client.get_status()
  140. assert isinstance(st, dict)
  141. assert st['connected'] is False
  142. # 连接后
  143. self.mqtt_client.connect(
  144. broker='localhost', port=1883, client_id='test_client', keepalive=60
  145. )
  146. st = self.mqtt_client.get_status()
  147. assert isinstance(st, dict)
  148. assert st['connected'] is True
  149. # 断开后
  150. self.mqtt_client.disconnect()
  151. st = self.mqtt_client.get_status()
  152. assert st['connected'] is False
  153. @patch('modules.mqtt_client.mqtt.Client')
  154. def test_on_message_callback(self, mock_client_class):
  155. """测试MQTT消息回调处理"""
  156. mock_client = _setup_mock_client(mock_client_class)
  157. self.mqtt_client.connect(
  158. broker='localhost', port=1883, client_id='test_client', keepalive=60
  159. )
  160. on_message_callback = mock_client.on_message
  161. mock_message = MagicMock()
  162. mock_message.topic = "test/topic"
  163. mock_message.payload = b"test payload"
  164. mock_message.qos = 0
  165. mock_message.retain = False
  166. on_message_callback(mock_client, None, mock_message)
  167. self.data_callback.assert_called_once()
  168. call_data = self.data_callback.call_args[0][0]
  169. assert call_data['topic'] == 'test/topic'
  170. assert call_data['payload'] == 'test payload'
  171. if __name__ == "__main__":
  172. pytest.main(["-v", __file__])