26、自动化测试与CI/CD:Qt Test 单元测试、Squish GUI测试、Jenkins流水线集成
说实话,车载项目里最怕什么?不是功能做不出来,而是改了一个小地方,结果把别的地方搞崩了。我见过太多团队,测试全靠人工点点点,最后发布前通宵加班。这章咱们就聊聊怎么用自动化测试和CI/CD把这套流程管起来。
核心思路:单元测试保底层逻辑,GUI测试保交互流程,CI/CD保持续集成。三层防护,缺一不可。
26.1 Qt Test 单元测试:给代码上保险
Qt Test 是 Qt 自带的单元测试框架。说白了,就是写一些测试用例,验证你的函数、类在给定输入下能不能输出正确结果。我个人的习惯是,每写一个核心功能模块,顺手就把单元测试写了。别拖,一拖就忘了。
来看个最简单的例子。假设你写了个计算油耗的类:
// fuelcalculator.h
class FuelCalculator {
public:
double calculateConsumption(double distance, double fuelUsed);
};
// fuelcalculator.cpp
double FuelCalculator::calculateConsumption(double distance, double fuelUsed) {
if (distance <= 0 || fuelUsed <= 0) return -1.0;
return (fuelUsed / distance) * 100.0;
}
对应的单元测试怎么写?
// test_fuelcalculator.cpp
#include <QtTest>
#include "fuelcalculator.h"
class TestFuelCalculator : public QObject {
Q_OBJECT
private slots:
void testNormalCase();
void testZeroDistance();
void testNegativeValues();
};
void TestFuelCalculator::testNormalCase() {
FuelCalculator calc;
double result = calc.calculateConsumption(500.0, 40.0);
QCOMPARE(result, 8.0); // 百公里8升
}
void TestFuelCalculator::testZeroDistance() {
FuelCalculator calc;
double result = calc.calculateConsumption(0.0, 40.0);
QCOMPARE(result, -1.0); // 非法输入返回-1
}
void TestFuelCalculator::testNegativeValues() {
FuelCalculator calc;
double result = calc.calculateConsumption(-100.0, 40.0);
QCOMPARE(result, -1.0);
}
QTEST_MAIN(TestFuelCalculator)
#include "test_fuelcalculator.moc"
小技巧:QCOMPARE 宏会自动打印预期值和实际值,调试时非常方便。我一般还会配合 QBENCHMARK 宏做性能测试,看看函数跑得快不快。
运行测试也很简单:
qmake test_fuelcalculator.pro
make
./test_fuelcalculator
输出结果会告诉你哪些用例通过了,哪些挂了。嗯,这里要注意:单元测试一定要跑在无头环境里,不然 Jenkins 上跑不了。
26.2 Squish GUI测试:模拟人的操作
单元测试能测逻辑,但测不了界面。比如按钮点没点中、滑动条滑没滑到位、弹窗有没有正常显示——这些就得靠 GUI 自动化测试工具。我项目中用得最多的是 Squish,Qt 官方也推荐它。
Squish 的原理很简单:录制你的操作,然后回放。但实际项目中,我更建议手写脚本,因为录制的脚本太啰嗦,维护起来头疼。
来看一个 Squish 测试脚本的例子,模拟用户点击导航菜单:
# test_navigation.py (Squish脚本)
import names
def main():
# 启动应用
startApplication("my_car_app")
# 等待主界面加载
snooze(2)
# 点击导航按钮
clickButton(waitForObject(names.nav_button))
# 输入目的地
typeText(waitForObject(names.destination_input), "北京南站")
# 点击搜索
clickButton(waitForObject(names.search_button))
# 验证结果列表出现
test.compare(
waitForObjectExists(names.result_list).visible,
True,
"搜索结果列表应该可见"
)
# 截图保存证据
test.log("导航测试通过", test.SCREENSHOT)
避坑指南:我曾经在项目里吃过亏——Squish 脚本跑得好好的,换了一台分辨率不同的设备就全挂了。后来学乖了,所有对象都用 waitForObject 配合属性匹配,别用坐标。坐标是魔鬼。
Squish 支持 Python 和 JavaScript 两种脚本语言。我个人偏爱 Python,因为 Qt 的 QML 对象树用 Python 操作起来更顺手。你想想看,一个车载 HMI 界面可能有上百个控件,用 Python 写循环、条件判断都方便得多。
26.3 Jenkins流水线集成:让一切自动跑起来
单元测试写了,GUI 测试也写了,但每次都要手动跑一遍?那还不如不写。真正的价值在于——每次代码提交,自动触发测试。这就是 CI/CD 干的事。
我常用的 CI 工具是 Jenkins。配置一个流水线(Pipeline)大概分三步:
- 拉代码:从 Git 仓库拉取最新代码
- 编译+跑单元测试:用 qmake 或 cmake 编译,然后执行 Qt Test
- 跑 Squish GUI 测试:启动 Squish 服务器,执行测试套件
下面是一个 Jenkinsfile 的简化版:
pipeline {
agent any
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build & Unit Test') {
steps {
sh 'mkdir build && cd build && cmake .. && make'
sh './build/test_fuelcalculator' // 跑Qt Test
}
post {
always {
junit 'build/test_results.xml' // 生成测试报告
}
}
}
stage('GUI Test') {
steps {
sh 'squishrunner --testsuite /path/to/suite --reportgen xml,/tmp/report'
}
post {
always {
junit '/tmp/report/*.xml'
}
}
}
}
post {
failure {
emailext(
subject: "构建失败: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
body: "请检查 Jenkins 构建结果。",
to: "team@example.com"
)
}
}
}
经验之谈:Jenkins 里跑 Squish 需要配置无头显示。我一般用 Xvfb(虚拟帧缓冲)来模拟显示器。命令大概是 xvfb-run squishrunner ...。不然 Jenkins 服务器没接显示器,Squish 会报错。
26.4 整体流程:一张图说清楚
下面这张 SVG 图展示了从代码提交到测试报告的全流程。我习惯把它贴在团队的白板上,谁都能一眼看懂。
26.5 常见问题与避坑
做这套东西,坑不少。我挑几个典型的说说:
- 测试环境不一致:开发机是 Ubuntu 22.04,Jenkins 是 CentOS 7,Qt 版本不一样,跑出来结果不同。我的做法是用 Docker 容器统一环境,Jenkins 里直接拉镜像跑。
- Squish 对象识别失败:QML 控件如果没设置 objectName,Squish 就抓不到。我要求团队所有可交互控件必须显式设置 objectName,这是硬性规定。
- 测试跑太久:全量 GUI 测试一次可能要 40 分钟。我建议分两层——提交时跑冒烟测试(10 分钟以内),每晚跑全量回归。
- 误报:有时候测试本身不稳定,比如网络延迟导致截图对比失败。我加了重试机制,失败一次自动重跑两次,三次都失败才算真失败。
曾经踩过的坑:有一次 Jenkins 上单元测试全绿,但 Squish 测试全红。查了半天,发现是 Jenkins 服务器时区不对,导致时间戳相关的 UI 显示异常。从那以后,我所有测试脚本里都显式设置时区,不依赖系统默认。
26.6 总结
自动化测试和 CI/CD 这件事,说白了就是用机器的稳定换人的不稳定。你想想看,人工测试 100 个用例,第 50 个可能就走神了。机器不会。虽然前期搭建要花点功夫,但一旦跑起来,省下的时间绝对值得。
我个人建议,先从 Qt Test 单元测试入手,把核心逻辑覆盖了。然后挑几个高频的 GUI 操作场景用 Squish 写脚本。最后用 Jenkins 串起来。别想一口吃成胖子,一步一步来。
一句话记住:单元测试保底,GUI 测试保面,CI/CD 保流程。三管齐下,车载项目才能睡得着觉。