上位机开发日记 · 第 12 篇 · 测试:设备模拟器是核心资产

阅读时长 :约 5 分钟 · 难度 :进阶 · 前置知识 :第 5 篇(解帧器)、第 9 篇(健壮性)
能装上了,本篇解决"怎么证明它是对的"。
1. 上位机测试的特殊困难
普通软件的测试,跑起来就行。上位机的测试有个绕不开的麻烦:所有功能都依赖一个不在你手里的硬件。
于是常见的情况是:
| 借口 | 后果 |
|---|---|
| "设备还没到,先写界面吧" | 界面写完了,协议一接全错(第 3 篇已讲过) |
| "现场设备不能随便断线测试" | 断线逻辑从没被验证过,第一次触发就是现场故障 |
| "没法复现那个 bug,数据没存下来" | 同样的问题反复出现 |
破解办法只有一个:把硬件从测试环节里去掉。 做两件事------单元测试覆盖协议,模拟器替代设备。
2. 上位机的测试金字塔

【判断】和其他软件不同,上位机最有价值的测试在底部和顶部:
- 底部(协议单元测试):分支多、成本低、收益高,见第 5 篇的 11 个用例。
- 顶部(长稳测试):唯一能发现内存泄漏、缓冲溢出、句柄耗尽这类问题的测试。
而中间层的集成测试,恰好可以用模拟器廉价地实现。
3. 协议单元测试
这是投入产出比最高的一类测试。要求是:协议层代码必须能脱离硬件运行(回顾第 2 篇的分层------这正是分层的回报)。
第 5 篇已经给出了 11 个用例的完整实现,其中两个最关键:
| 用例 | 为什么关键 |
|---|---|
帧头被切断 (0xAA 和 0x55 分在两批数据里) |
概率低但一定会遇到,且极难在现场复现 |
| 假帧头(长度字段越界) | 会导致缓冲区被彻底堵死,现象是"完全收不到数据" |
这两个用例的存在本身,就是分层的价值证明------没有分层,协议代码和串口绑在一起,你根本没法构造这两种输入。
4. 设备模拟器:怎么设计才有用
模拟器不是"随便发点数据",它要能主动制造各种异常。设计成命令行可配置的:
| 参数 | 作用 | 验证什么 |
|---|---|---|
--rate 100 |
每秒发 100 帧 | 正常开发 |
--inject crc |
每 N 帧注入一个坏 CRC | 解帧器的坏帧处理与计数 |
--inject split |
把一帧拆成两批发送 | 半帧处理(第 5 篇用例 6 的现场版) |
--inject garbage |
随机插入噪声字节 | 帧头重同步 |
--drop-prob 0.01 |
随机断线 | 重连与退避逻辑(第 9 篇) |
--silent 30 |
静默 30 秒 | 假死检测(第 9 篇) |
--playback file.bin |
回放录制数据 | 复现现场问题 |
--burst |
突发高速发送 | 背压与缓冲策略(第 6 篇) |
一个最小骨架:
python
import argparse, time
from frame_parser import build_frame # 复用同一份格式定义!
p = argparse.ArgumentParser()
p.add_argument("--rate", type=float, default=10.0)
p.add_argument("--inject", choices=["none", "crc", "split", "garbage"], default="none")
args = p.parse_args()
interval = 1.0 / args.rate
seq = 0
while True:
payload = seq.to_bytes(2, "little") + b"\x00" * 6
frame = build_frame(payload)
if args.inject == "crc":
frame = frame[:-1] + bytes([frame[-1] ^ 0xFF])
elif args.inject == "split":
mid = len(frame) // 2
send(frame[:mid]); time.sleep(0.005); frame = frame[mid:]
elif args.inject == "garbage":
send(b"\x00\xFF" * 3)
send(frame)
seq += 1
time.sleep(interval)
三个设计要点:
| 要点 | 原因 |
|---|---|
复用 build_frame() |
与上位机共用同一份格式定义,避免"模拟器和上位机一起错" |
| 可注入异常 | 否则模拟器只能测通顺路径,等于没测健壮性 |
| 可回放 | 现场 bug 的第一手证据就是那段字节流 |
【判断】模拟器和上位机必须一起进版本管理。现场出了问题,让你复现的第一步不是"再连一次设备试试",而是"把当时的字节流喂给模拟器重放一遍"。
5. 虚拟串口:无硬件联调
需要真实串口设备、但手上没有时,可以造一对虚拟串口。
| 平台 | 工具 | 命令示意 |
|---|---|---|
| macOS / Linux | socat |
socat -d -d pty,raw,echo=0 pty,raw,echo=0 |
| Windows | com0com | 图形界面创建配对的虚拟 COM 口 |
两端一个给模拟器用,一个给上位机用,就形成了一条完整的"软件闭环链路"。
【判断】这条链路强烈建议固化进项目。它让新同事第一天就能跑通全流程,不需要等设备、不需要占工位。
6. 长稳测试与对账
单元测试和模拟器都不能发现的问题:内存缓慢增长、文件句柄泄漏、长时间后的缓冲区退化。
方法只有一个:连续跑。
| 项目 | 要求 |
|---|---|
| 时长 | 8 小时以上;关键项目建议 24 小时 |
| 监控 | 内存占用、CPU 占用、日志文件大小、缓存残留量 |
| 对账 | 收帧数 - 坏帧数 = 存盘帧数(回顾第 7 篇) |
| 通过标准 | 数据对账闭合 + 内存曲线平稳 |
【判断】长稳测试期间要有人能中途看一眼(或者做自动断言)。跑了一夜之后再看,如果发现对不上账,你很难判断是从第几分钟开始错的。
7. 回放测试:把现场带回家
现场数据是最好的测试数据。做法:
- 在采集程序里加一个"原始字节转储"开关,把原始流按时间戳写进文件;
- 出问题时,让对方把文件发过来;
- 用模拟器的
--playback模式重放,在你自己的机器上完整复现。
【判断】"能不能复现"是区分专业和业余的关键。 有了回放能力,你就不用一遍遍远程指挥现场同事"再试一下、再看看",而是直接在自己机器上调试。这是本系列里性价比最高的能力之一。
8. 本篇小结
| 记住这一条 | 说明 |
|---|---|
| 把硬件从测试里去掉 | 单元测试 + 模拟器 |
| 协议单元测试优先 | 分支多、成本低、收益高 |
| 模拟器要能注入异常 | 只会发正常帧的模拟器没有价值 |
复用 build_frame() |
保证模拟器与上位机格式一致 |
| 虚拟串口做闭环 | 新同事第一天就能跑通 |
| 长稳测试 + 对账 | 唯一能发现泄漏类问题的方法 |
| 保留回放能力 | 能复现,才有资格谈排查 |
9. 动手练习
-
生成两个"残缺"的测试用例:把你的解帧器改成只保留 9 个用例(去掉第 6、9 项),然后手动构造这两种情况------如果你之前没测过这两个场景,很可能会发现真的有问题。
-
写一个最小模拟器 :只用
build_frame()+ 一个while循环 +--rate参数。先跑起来,再逐步加--inject。重点是先有,再加料。 -
打开字节转储:给你的采集程序加一个开关,把原始字节流带时间戳写进文件。就加这一个功能,你的排障能力会当场升级。
上一篇 :第 11 篇 · 打包与发布:PyInstaller 与 Nuitka
下一篇 :第 13 篇 · 坑位清单:16 个高频问题
------把前面所有内容收成一张可以贴在工位上的对照表。