Pelco KBD300A 模拟器:20.搭建pytest集成测试基础框架 + 模拟器闭环测试

第 20 篇:搭建集成测试基础框架 + 模拟器闭环测试

下面我们将一步一步详细完成集成测试基础框架搭建 ,并实现模拟器闭环测试(串口收到数据 → 协议解析 → VirtualDevice 状态更新 → 可选返回响应)。

目标:

  • 不依赖真实串口硬件
  • 使用 mock 完全控制输入/输出
  • 验证 Pelco-D/P 命令到达 VirtualDevice 后状态是否正确变化
  • 为后续宏 → 串口 → 模拟器链路打基础
20.1 项目结构调整(添加 integration 目录)

在项目根目录下创建以下结构(如果还没有):

复制代码
KBD300A/
├── core/
│   ├── protocol/
│   ├── macro/
│   ├── serial/
│   ├── simulator/
│   └── ...
├── ui/
├── config/
├── resources/
├── tests/                     ← 新建
│   ├── unit/                  ← 已有的单元测试
│   ├── integration/           ← 集成测试放这里
│   │   ├── test_simulator_loop.py
│   │   ├── test_macro_to_serial.py
│   │   └── ...
│   ├── conftest.py            ← 全局 fixture
│   └── test_data/             ← 测试用 Pelco 命令样本
└── requirements-dev.txt       ← 测试依赖

requirements-dev.txt(安装这些包,Python 3.7 兼容):

复制代码
pytest==6.2.5
pytest-mock==3.6.1
pytest-qt==4.0.2          # 如果涉及 Qt 信号等待
coverage==5.5             # 可选,覆盖率统计

安装:

bash 复制代码
pip install -r requirements-dev.txt
20.2 创建 conftest.py(全局 fixture)

tests/conftest.py

python 复制代码
# tests/conftest.py
import pytest
from unittest.mock import MagicMock
from core.simulator.virtual_device import VirtualDevice
from core.protocol import get_protocol
from core.serial.manager import SerialManager

@pytest.fixture
def virtual_device():
    """每个测试一个干净的虚拟设备"""
    return VirtualDevice(cam_id=1)

@pytest.fixture
def mock_serial():
    """模拟 serial.Serial 对象"""
    mock_ser = MagicMock()
    mock_ser.is_open = True
    mock_ser.in_waiting = 0
    mock_ser.read.side_effect = lambda size: b""  # 默认返回空
    mock_ser.write = MagicMock()
    return mock_ser

@pytest.fixture
def mock_serial_manager(mock_serial):
    """模拟 SerialManager,注入 mock 串口"""
    manager = SerialManager(port="COMfake", baud=9600, protocol="D")
    manager._ser = mock_serial  # 直接替换底层串口
    manager.is_open = True
    return manager

@pytest.fixture
def pelco_d_protocol(mock_serial_manager):
    """Pelco-D 协议实例"""
    return get_protocol(mock_serial_manager, "D")

@pytest.fixture
def pelco_p_protocol(mock_serial_manager):
    """Pelco-P 协议实例"""
    return get_protocol(mock_serial_manager, "P")
20.3 准备测试数据(Pelco 命令样本)

tests/test_data/pelco_commands.json 中保存一些典型命令(方便复用):

json 复制代码
{
  "pelco_d": {
    "ptz_right_fast": {
      "bytes": "FF0100001020004F",
      "description": "地址1,右转速度32",
      "expected_pan": 32.0
    },
    "call_preset_5": {
      "bytes": "FF010000071F0067",
      "description": "调用预置位5",
      "preset_id": 5
    },
    "aux_on_2": {
      "bytes": "FF0100000B02000E",
      "description": "AUX2 开"
    }
  },
  "pelco_p": {
    "ptz_up_medium": {
      "bytes": "A0010008100800AF",
      "description": "地址1,上仰速度8"
    }
  }
}

加载方式示例:

python 复制代码
import json
from pathlib import Path

TEST_DATA = json.loads(Path("tests/test_data/pelco_commands.json").read_text())
20.4 实现模拟器闭环测试(核心文件)

tests/integration/test_simulator_loop.py

python 复制代码
# tests/integration/test_simulator_loop.py
import pytest
import time
from unittest.mock import call
from core.serial.worker import SerialWorker
from core.protocol.pelco_d import PelcoDProtocol
from core.simulator.virtual_device import VirtualDevice
from tests.conftest import mock_serial_manager  # 如果需要

def load_command_hex(hex_str):
    """将 hex 字符串转为 bytes"""
    return bytes.fromhex(hex_str)

@pytest.mark.integration
def test_pelco_d_ptz_command_updates_virtual_device(virtual_device, mock_serial, pelco_d_protocol):
    """
    测试:收到 Pelco-D 右转命令 → VirtualDevice pan 角度增加
    """
    # 准备输入:右转速度 32 的命令
    cmd_hex = "FF 01 00 00 20 00 21"  # 校验和 0x21 = 1+0+0+32+0
    cmd_bytes = load_command_hex(cmd_hex.replace(" ", ""))

    # 模拟 SerialWorker 收到数据(直接注入 buffer)
    worker = SerialWorker(port="COMfake", baud=9600, protocol="D")
    worker._ser = mock_serial
    worker._buffer = bytearray(cmd_bytes)

    # 关键:注入 VirtualDevice 到协议层(需要小改 protocol 代码支持)
    # 临时方案:在测试中 monkey-patch 或直接调用 process
    pelco_d_protocol.virtual_device = virtual_device  # 假设已添加属性

    # 模拟一次数据处理(实际项目中可调用 worker._process_buffer())
    parsed = pelco_d_protocol.parse_response(cmd_bytes)

    # 或者更真实:模拟 worker 处理流程
    worker._buffer.extend(cmd_bytes)
    worker._process_buffer()  # 如果 _process_buffer 是 public 或可访问

    # 检查 VirtualDevice 状态
    status = virtual_device.get_status_dict()
    
    assert "pan" in status
    pan_value = float(status["pan"].rstrip("°"))
    assert pan_value > 0, "右转命令应使 pan 角度增加"

    # 可选:验证具体角度(根据 VirtualDevice 内部实现比例)
    # 假设 VirtualDevice 将 speed 32 映射为 +32 度
    assert abs(pan_value - 32.0) < 5.0, f"预期 pan ≈32,实际 {pan_value}"

@pytest.mark.integration
def test_pelco_d_call_preset_triggers_action(virtual_device, mock_serial, pelco_d_protocol):
    """测试调用预置位命令是否触发 VirtualDevice 相应逻辑"""
    cmd_hex = "FF 01 00 00 07 05 0D"  # 调用预置位 5,校验和 1+0+0+7+5=13=0x0D
    cmd_bytes = load_command_hex(cmd_hex.replace(" ", ""))

    # 注入
    pelco_d_protocol.virtual_device = virtual_device

    # 处理命令
    worker = SerialWorker(port="COMfake", baud=9600, protocol="D")
    worker._ser = mock_serial
    worker._buffer = bytearray(cmd_bytes)
    worker._process_buffer()  # 或直接调用协议 parse

    # 假设 VirtualDevice 在 process_command 中记录 preset 调用
    # 你可以添加断言,例如检查日志或状态
    # 或者临时添加一个调用计数器属性到 VirtualDevice
    assert hasattr(virtual_device, "last_preset_called")
    assert virtual_device.last_preset_called == 5

@pytest.mark.integration
def test_pelco_p_aux_on_updates_aux_state(virtual_device, mock_serial):
    """Pelco-P 打开 AUX2 测试"""
    cmd_hex = "A0 01 00 0B 02 00 0E AF"  # AUX2 ON
    cmd_bytes = load_command_hex(cmd_hex.replace(" ", ""))

    protocol = PelcoDProtocol(mock_serial)  # 改用 P 协议
    protocol.virtual_device = virtual_device

    # 处理
    parsed = protocol._parse_response(cmd_bytes)
    assert parsed.get("type") == "aux"
    assert parsed.get("aux_id") == 2
    assert parsed.get("state") == "on"

    # 验证虚拟设备状态
    assert virtual_device.aux_states[2] is True
20.5 小幅修改生产代码以便测试

core/simulator/virtual_device.py 中添加:

python 复制代码
class VirtualDevice:
    def __init__(self, cam_id: int = 1):
        ...
        self.last_preset_called = None  # 测试用

    def process_command(self, data: bytes) -> Optional[bytes]:
        parsed = parse_pelco_packet(data)
        if parsed.get("type") == "preset" and parsed.get("operation") == "call":
            self.last_preset_called = parsed.get("preset_id")
        # ... 原有逻辑

core/protocol/base.py 或具体协议类中添加:

python 复制代码
class ProtocolBase(ABC):
    def __init__(self, serial_mgr):
        self.serial_mgr = serial_mgr
        self.virtual_device = None  # 测试时注入
20.6 运行测试
bash 复制代码
# 在项目根目录执行
pytest tests/integration/ -v --tb=short

# 只跑模拟器闭环测试
pytest tests/integration/test_simulator_loop.py -v

# 带覆盖率
pytest --cov=core --cov-report=html tests/integration/

👉上一篇 :pytest集成测试(serial + protocol + macro)

👉总目录:Python开发软键盘全程总览

👉下一篇:

相关推荐
2301_790300966 小时前
Python单元测试(unittest)实战指南
jvm·数据库·python
Wang201220136 小时前
芯片serdes phy vth下阈值过低,线缆干扰会识别成oob如何解决
集成测试
VCR__6 小时前
python第三次作业
开发语言·python
韩立学长6 小时前
【开题答辩实录分享】以《助农信息发布系统设计与实现》为例进行选题答辩实录分享
python·web
2401_838472516 小时前
使用Scikit-learn构建你的第一个机器学习模型
jvm·数据库·python
u0109272716 小时前
使用Python进行网络设备自动配置
jvm·数据库·python
工程师老罗6 小时前
优化器、反向传播、损失函数之间是什么关系,Pytorch中如何使用和设置?
人工智能·pytorch·python
Fleshy数模6 小时前
我的第一只Python爬虫:从Requests库到爬取整站新书
开发语言·爬虫·python
CoLiuRs6 小时前
Image-to-3D — 让 2D 图片跃然立体*
python·3d·flask
小鸡吃米…7 小时前
机器学习 —— 训练与测试
人工智能·python·机器学习