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)大概分三步:

  1. 拉代码:从 Git 仓库拉取最新代码
  2. 编译+跑单元测试:用 qmake 或 cmake 编译,然后执行 Qt Test
  3. 跑 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 图展示了从代码提交到测试报告的全流程。我习惯把它贴在团队的白板上,谁都能一眼看懂。

自动化测试与CI/CD流水线 1. 代码提交 Git Push / PR 2. Jenkins触发 Webhook / 定时 3. 编译构建 cmake / qmake 4. Qt Test 单元测试 逻辑验证 · 性能基准 5. Squish GUI测试 交互验证 · 截图对比 6. 静态代码分析 Clang-Tidy · QML Lint 7. 生成报告 & 通知 邮件 · 钉钉 · 企业微信 CI流程 测试阶段 输出阶段

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 保流程。三管齐下,车载项目才能睡得着觉。


公众号:蓝海资料掘金营,微信 deep3321