天外客AI翻译机自动化测试覆盖率提升实践

在智能硬件的战场上,谁先拿下“质量高地”,谁就掌握了用户信任的入场券。
可现实是:功能越炫,系统越复杂;迭代越快,出错越多 😣。

“天外客AI翻译机”就是这样一个典型——集语音识别、机器翻译、离线推理、蓝牙通信于一体的小巧设备,背后却是嵌入式系统、移动端App和云端服务交织的“代码迷宫”。
每次发版前,团队都像在拆炸弹:剪哪根线?会不会炸?没人敢打包票 💣。

直到我们痛定思痛:不能再靠人肉点了!必须把 自动化测试覆盖率 从48%拉上去,否则根本扛不住每周一次的需求变更潮。

于是,一场关于“如何让代码自己说话”的工程战役打响了。


从“测不到”到“看得见”:全栈覆盖率工具链搭建

过去,我们的测试报告就像盲人摸象——Android端能看到一点,Python后端勉强有数,C语言写的固件?纯黑盒 🤷‍♂️。
不同模块用不同的工具、格式、标准,根本没法统一衡量。

怎么办? 打通任督二脉,建立全栈可视化的覆盖率体系 !

移动端(Java/Kotlin)→ JaCoCo 插桩监控

Android这边我们上了 JaCoCo ,它能在编译时对字节码插桩,运行测试时自动记录哪些代码被执行过。关键是—— 完全不影响正常逻辑 ,就像给程序装了个隐形追踪器 🔍。

# .gitlab-ci.yml 片段
unit-test:
  script:
    - coverage run -m pytest tests/unit/
    - coverage xml -o coverage-python.xml
    - coverage report

integration-test:
  script:
    - ./gradlew connectedAndroidTest
    - ./gradlew jacocoTestReport

CI流水线上,只要跑一遍连接测试, .exec 文件自动生成,配合 Gradle 插件还能直接输出 HTML 报告,连 QA 小姐姐都能看懂哪块没覆盖 👩‍💻。

Python服务端 → coverage.py 轻量级监听

后端用的是 FastAPI + Celery 的微服务架构,测试本来就以 pytest 为主。接入 coverage.py 几乎零成本:

coverage run -m pytest tests/
coverage html  # 生成可视化报告

一行命令搞定,还能设置阈值门禁:“新增代码覆盖率低于75%,PR直接拒绝!”
从此,没人敢再提交“裸奔代码”了 ✅。

嵌入式C模块 → GCC + lcov 实现物理突破

最难啃的骨头来了:STM32 上跑的 C 固件,怎么测覆盖率?

传统做法是上真实设备 + 串口打印,但覆盖率数据根本拿不到。后来我们灵机一动: 能不能在虚拟环境里跑带覆盖率采集的固件?

答案是——能!用 QEMU 搭建 ARM 虚拟机 ,配合 GCC 的 --coverage 编译选项:

arm-none-eabi-gcc --coverage -o firmware.elf main.c

这样运行时会自动生成 .gcda 和 .gcno 文件,再用 lcov 转成 HTML 报告:

lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory ./report

虽然过程有点“硬核”,但效果惊人:原本只有不到30%的覆盖率,现在稳定在 76%以上 ,还挖出了几个藏了半年的边界空指针 bug ⚡️。

💡 小贴士:我们在 GDB 里写了脚本,在特定断点自动 dump 内存中的 .gcda 数据,避免程序退出前没来得及写文件的问题。


分层测试不是口号,是效率命脉

早期我们走了一段弯路:为了“看起来全面”,写了大量端到端(E2E)测试,结果 CI 流水线动不动卡40分钟,开发者苦不堪言 😫。

后来我们重新审视了 测试金字塔模型 ,并真正把它落地了:

层级 类型 占比 工具 执行时间
L1 单元测试 ~70% pytest/unittest/mock <2min
L2 集成测试 ~20% Postman + Docker 3~8min
L3 E2E测试 ~10% Appium + 自研设备控制器 5~15min

别小看这个比例调整,带来的变化是颠覆性的:

  • 反馈更快 :本地开发时跑L1测试秒出结果,修完立马验证;
  • 定位更准 :失败了知道是算法问题还是接口问题,不用再猜;
  • 资源更省 :90%以上的测试都在模拟器或容器中完成,不再抢真实设备。

特殊场景怎么破?

AI模型频繁更新 → 影子测试 + 黄金样本比对

模型团队两周一迭代,传统功能测试根本跟不上节奏。怎么办?

我们搞了个“影子测试”机制:

  1. 构建一组 固定输入样本库 (含方言、噪声、语速变化等);
  2. 每次新模型上线前,用这些样本跑一遍,记录输出文本和语音频谱;
  3. 和旧模型做对比,计算 BLEU/WER 分数;
  4. 设定容忍阈值(如 WER ≤ 5%),超标即告警。

有一次就是因为量化压缩过度,导致粤语识别准确率暴跌,被这套机制当场拦截 ❌。
要不是有这道防线,估计下一波用户投诉就要刷屏了。

音频处理链路难验证 → MFCC特征图谱比对

还有个头疼的问题:音频信号经过降噪、VAD、编码等一系列处理,最后输出的声音听起来差不多,但内部可能已经“变味”了。

解决方案? 用MFCC(梅尔频率倒谱系数)做指纹比对 !

我们预先录制一批“黄金样本”,提取其原始MFCC图谱作为基准。每次测试运行后,再次提取处理后的MFCC,用余弦相似度打分:

from sklearn.metrics import cosine_similarity
import librosa

def extract_mfcc(audio_path):
    y, sr = librosa.load(audio_path)
    return librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)

similarity = cosine_similarity([mfcc_old], [mfcc_new])[0][0]
assert similarity > 0.95  # 至少95%相似

这样一来,哪怕听不出差别,也能从数学上证明处理流程的一致性 ✅。


让测试“活”起来:Mock Server + 自动化注入

真正的高覆盖率,不只是“跑起来”,而是要 覆盖那些你想躲都躲不掉的异常路径 。

比如:
- 翻译API返回503?
- 蓝牙突然断连?
- 设备电量不足触发休眠?

这些问题在手动测试中很难稳定复现,但在自动化里,我们可以“想让它发生,它就得发生”。

Mock Server:掌控一切网络响应

我们基于 Flask 搭了个轻量级 Mock Server,专门拦截 App 与云端之间的所有 HTTP 请求:

from flask import Flask, jsonify, request

app = Flask(__name__)

@app.route('/api/translate', methods=['POST'])
def translate():
    if request.headers.get("X-Simulate-Error") == "503":
        return jsonify({"error": "Service Unavailable"}), 503
    return jsonify({"text": "Hello world"})

if __name__ == '__main__':
    app.run(port=8080)

然后在测试用例中动态控制:

def test_realtime_translation_with_network_error(driver):
    requests.post("http://mock:8080/set-behavior", json={
        "endpoint": "/translate",
        "status": 503,
        "delay": 2
    })

    record_btn = driver.find_element_by_id("record_button")
    record_btn.click()

    error_toast = driver.find_element_by_xpath("//android.widget.Toast")
    assert "服务不可用" in error_toast.text

通过这种方式,我们轻松覆盖了超时、重试、降级、缓存命中失败等各种边缘情况,异常路径覆盖率提升了整整 34个百分点 !

设备行为注入:让硬件“听话”

更进一步,我们还实现了对翻译机本身的精准控制。

通过 ADB + UART 串口通信,编写了一个 设备仿真控制器 ,可以远程发送指令:

  • 模拟按键按下
  • 注入假的电池电压
  • 强制重启/进入DFU模式
  • 触发OTA升级中断

比如这个测试用例,专门验证“升级过程中断电恢复”逻辑:

def test_ota_resume_after_power_loss(controller, api_client):
    controller.start_ota(firmware="v2.1.bin")
    time.sleep(3)
    controller.cut_power()  # 断电
    time.sleep(2)
    controller.restore_power()

    status = api_client.get_update_status()
    assert status == "resuming"  # 应该从中断处继续

这种级别的控制力,才是高质量固件测试的底气所在。


不只是数字游戏:覆盖率背后的质变

当自动化测试覆盖率从 48% 提升到 87.3% 的那一刻,我们并没有庆祝,因为真正的改变早已悄然发生:

🔧 生产缺陷下降68% :大多数严重问题都在CI阶段被拦下,发布后崩溃率明显降低;
🚀 版本周期缩短一半 :从每6周一次发布,变成每3周就能上线一个稳定版本;
🧠 团队信心增强 :以前改核心模块如履薄冰,现在敢大胆重构,反正测试兜底;
📈 质量文化成型 :新人入职第一课就是“写代码前先想测试”。

更重要的是,我们建立了一套 可持续演进的质量基础设施 :

  • 所有覆盖率数据汇总到 SonarQube,生成热力图,一眼看出薄弱模块;
  • 支持增量分析,只关注本次修改涉及的代码覆盖率变化;
  • Jenkins 定时发送趋势报告,管理层也能看清技术债走势。

写在最后:测试不是成本,是加速器

很多人 still think 测试是拖慢进度的累赘,但我们发现恰恰相反——
高质量的自动化测试,其实是研发速度的“涡轮增压” 💨。

它让你敢于快速迭代,因为它给了你“踩刹车”的能力。

“天外客AI翻译机”的这段旅程告诉我们:

✅ 工具链要打通,不能各管一段;
✅ 测试要分层,不能全堆在顶层;
✅ 覆盖率要有门禁,不能形同虚设;
✅ 最关键的是: 要把测试当成产品的一部分来经营 。

如今,每当一个新的 PR 被合并,Jenkins 都会安静地跑完上千个测试用例,生成一份清晰的覆盖率报告。
而我们,可以安心地说一句:

“这版,能发。” ✅🎉

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐