Qt Test 深入:从 QCOMPARE 到数据驱动与基准测试
Qt Test 是 Qt 自带的单元测试框架,随 Qt 一起分发,不需要额外依赖。它的定位很明确:测试 C++ 类和函数的逻辑,尤其是那些和 QObject、信号槽、事件循环打交道的代码——这些用 Google Test 之类的通用框架测起来会很别扭。
一、最小可运行的测试
一个测试类就是一个继承 QObject 的类,私有槽就是测试函数:
#include <QtTest>
#include "calculator.h"
class TestCalculator : public QObject {
Q_OBJECT
private slots:
void initTestCase(); // 整个类跑之前,执行一次
void init(); // 每个测试函数之前
void cleanup(); // 每个测试函数之后
void cleanupTestCase(); // 整个类跑完,执行一次
void add(); // 真正的测试函数
void divideByZero();
private:
Calculator *m_calc = nullptr;
};
void TestCalculator::init() { m_calc = new Calculator; }
void TestCalculator::cleanup() { delete m_calc; m_calc = nullptr; }
void TestCalculator::add() {
QCOMPARE(m_calc->add(2, 3), 5);
QCOMPARE(m_calc->add(-1, 1), 0);
}
void TestCalculator::divideByZero() {
QVERIFY_EXCEPTION_THROWN(m_calc->divide(1, 0), std::invalid_argument);
}
QTEST_MAIN(TestCalculator) // 生成 main()
#include "test_calculator.moc" // 单文件写法必须包含
四个特殊函数的执行顺序是考点:initTestCase → (init → 测试函数 → cleanup) × N → cleanupTestCase。init/cleanup 每个用例都跑,用来保证用例之间互不污染。
另外三个 QTEST_* 宏的区别也常被问:
QTEST_MAIN— 创建QApplication(有 widgets 时)或QCoreApplicationQTEST_APPLESS_MAIN— 不创建应用对象,纯逻辑测试用,启动最快QTEST_GUILESS_MAIN— 创建QCoreApplication,有事件循环但无 GUI,适合网络/定时器测试
二、断言宏
QVERIFY(condition); // 条件为真
QVERIFY2(condition, "失败时的说明"); // 带描述,排查时省事
QCOMPARE(actual, expected); // 相等,失败时打印两边的值
QVERIFY_EXCEPTION_THROWN(expr, Type); // 必须抛出指定异常
QFAIL("不该走到这里");
QSKIP("依赖的服务未启动"); // 跳过,标记为 SKIP 而非 FAIL
QEXPECT_FAIL("", "已知 bug QTBUG-123", Continue); // 预期失败
优先用 QCOMPARE 而不是 QVERIFY(a == b)。前者失败时会打印 Actual: 4, Expected: 5,后者只告诉你条件为假,还得自己加日志。
另一个关键行为:QCOMPARE 失败会立即返回当前测试函数(宏里含 return),后续语句不执行。这和 Squish 的 test.compare 失败后继续跑恰好相反。
2.1 浮点数比较
QCOMPARE 对 float / double 会自动用模糊比较(qFuzzyCompare),相对误差容忍到大约 1e-5(float)和 1e-12(double)。这是好事,但也有个陷阱:qFuzzyCompare 不能处理 0,因为它算的是相对误差。比较结果是否为零要用 qFuzzyIsNull:
QCOMPARE(result, 0.0); // 可能意外失败 QVERIFY(qFuzzyIsNull(result)); // 正确写法
三、数据驱动测试
这是 Qt Test 最实用的特性。同一个测试逻辑跑多组输入,写法是给测试函数配一个 _data() 后缀的同名函数:
void TestCalculator::add_data() {
QTest::addColumn<int>("a");
QTest::addColumn<int>("b");
QTest::addColumn<int>("expected");
QTest::newRow("正数相加") << 2 << 3 << 5;
QTest::newRow("含负数") << -1 << 1 << 0;
QTest::newRow("零") << 0 << 0 << 0;
QTest::newRow("溢出边界") << INT_MAX << 0 << INT_MAX;
}
void TestCalculator::add() {
QFETCH(int, a); // 从当前数据行取值
QFETCH(int, b);
QFETCH(int, expected);
QCOMPARE(m_calc->add(a, b), expected);
}
好处是失败输出会带上行名(FAIL! : TestCalculator::add(溢出边界)),一眼看出是哪组数据挂了。行名要写得有意义,别用 row1 row2。
四、测试信号槽
这是 Qt Test 相比通用测试框架的核心优势。QSignalSpy 能记录信号的发射次数和每次的参数:
void TestContactModel::addContact() {
ContactModel model;
QSignalSpy insertSpy(&model, &ContactModel::rowsInserted);
QSignalSpy changeSpy(&model, &ContactModel::dataChanged);
model.addContact({"Jifang", "guo@example.com"});
QCOMPARE(insertSpy.count(), 1); // 发了一次
QCOMPARE(changeSpy.count(), 0); // 没有多余的信号
// 检查信号参数:take/at 返回 QList<QVariant>
const QList<QVariant> args = insertSpy.takeFirst();
QCOMPARE(args.at(1).toInt(), 0); // first row
QCOMPARE(args.at(2).toInt(), 0); // last row
}
异步信号要用 wait(),它内部会跑事件循环:
void TestDownloader::download() {
Downloader d;
QSignalSpy spy(&d, &Downloader::finished);
d.start(QUrl("http://example.com/file"));
// 等信号,最多 5 秒;Qt5.14+ 也可用 QVERIFY(spy.wait(5000))
QVERIFY(spy.wait(5000));
QCOMPARE(spy.count(), 1);
}
没有 QSignalSpy 的场景可以用 QTRY_COMPARE / QTRY_VERIFY,它们会在超时前反复重试并跑事件循环:
QTRY_COMPARE_WITH_TIMEOUT(model.rowCount(), 10, 3000); // 3 秒内变成 10 QTRY_VERIFY(widget->isVisible()); // 默认 5 秒
五、模拟 GUI 事件
Qt Test 也能做轻量的界面测试,注入的是真实的 Qt 事件:
void TestLoginDialog::submit() {
LoginDialog dlg;
dlg.show();
QVERIFY(QTest::qWaitForWindowExposed(&dlg)); // 必须等窗口真正显示
QTest::keyClicks(dlg.userEdit(), "admin");
QTest::keyClicks(dlg.passEdit(), "secret");
QTest::mouseClick(dlg.okButton(), Qt::LeftButton);
QCOMPARE(dlg.result(), QDialog::Accepted);
}
常用的注入函数:QTest::mouseClick / mouseDClick / mouseMove / mouseDrag、keyClick / keyClicks / keySequence。
两个要点。第一,qWaitForWindowExposed 不能省,窗口没显示完就发事件会丢。第二,这不能替代 Squish——它测的是单个 widget 的交互,测不了跨窗口的完整业务流程,也没有对象映射这类可维护性设施。
六、性能基准测试
QBENCHMARK 会自动多次运行代码块并取稳定值:
void TestParser::benchmarkParse() {
const QByteArray data = loadTestFile("large.json");
QBENCHMARK {
Parser::parse(data);
}
}
// 只测一次(适合本身耗时很长的操作)
void TestParser::benchmarkOnce() {
QBENCHMARK_ONCE { Parser::parse(hugeData); }
}
默认量的是墙上时钟(wall clock time,真实流逝时间)。命令行换后端能拿到更细的指标:-callgrind(Valgrind 工具集的指令计数器,最稳定,适合 CI 里做回归门槛)、-perf(Linux perf 性能事件)、-eventcounter(Qt 事件数)。
七、构建与运行
CMake 写法:
find_package(Qt6 REQUIRED COMPONENTS Test Core) enable_testing() qt_add_executable(test_calculator test_calculator.cpp calculator.cpp) target_link_libraries(test_calculator PRIVATE Qt6::Test Qt6::Core) add_test(NAME test_calculator COMMAND test_calculator)
常用命令行参数:
./test_calculator # 跑全部 ./test_calculator add # 只跑 add 这个函数 ./test_calculator "add:溢出边界" # 只跑某个数据行 ./test_calculator -v2 # 详细输出,含每个 QCOMPARE ./test_calculator -functions # 列出所有测试函数 ./test_calculator -o results.xml,junitxml # 输出 JUnit 给 CI ./test_calculator -maxwarnings 0 # 不限制警告数量
CI 里通常用 ctest --output-on-failure 统一驱动,失败时才打印详细输出。
八、局限与常见问题
- 没有内置 mock(模拟对象)框架。要打桩(stub)得自己写接口 + 派生假实现,或者引入 GMock(Google Mock)混用(技术上可行,但两套 main 函数要处理)
- 一个可执行文件通常只放一个测试类。想在一个 binary 里跑多个类需要自己写 main 依次
QTest::qExec,且信号槽的静态状态可能互相影响 - 测试函数必须是 private slots。写成 public slots 也能跑,但
signals或普通成员函数不会被识别,这是新手最常见的「测试没执行」原因 - QCOMPARE 自定义类型要提供
operator==,想让失败输出好看还得特化QTest::toString() - 依赖真实文件系统的测试记得用
QTemporaryDir,别往源码目录写文件
术语表
| 简称 | 英文全称 | 中文与说明 |
|---|---|---|
| — | unit test | 单元测试。针对单个类或函数的测试,与被测代码同进程 |
| — | test fixture | 测试夹具。initTestCase / init / cleanup / cleanupTestCase 四件套提供的测试环境准备与清理 |
| — | data-driven test | 数据驱动测试。_data() 函数配 QFETCH,同一逻辑跑多组输入 |
| — | fuzzy compare | 模糊比较。qFuzzyCompare 按相对误差比浮点数,不能用于 0 |
| — | signal spy | 信号侦听器。QSignalSpy 记录信号发射次数与参数 |
| — | benchmark | 基准测试。QBENCHMARK 多次运行取稳定值 |
| — | wall clock time | 墙上时钟时间。真实流逝时间,QBENCHMARK 的默认度量 |
| — | mock / stub | 模拟对象 / 打桩。替换真实依赖以隔离被测单元 |
| GMock | Google Mock | 谷歌的 C++ mock 框架,现已并入 GoogleTest |
| — | Valgrind / Callgrind | Linux 动态分析工具集 / 其中的指令计数器,用于稳定的性能回归度量 |
| CI | Continuous Integration | 持续集成 |
| CTest | — | CMake 自带的测试驱动器,统一执行并汇总测试结果 |
| JUnit XML | — | 源自 Java JUnit 框架的测试报告格式,已成为 CI 工具的事实标准 |
| GUI | Graphical User Interface | 图形用户界面 |
| moc | Meta-Object Compiler | 元对象编译器。测试类必须写 Q_OBJECT 并包含 .moc 文件 |
小结
Qt Test 的甜点区是「和 Qt 机制耦合的逻辑代码」:model 的数据正确性、信号的发射时机、异步操作的完成状态。QSignalSpy + 数据驱动 + QTRY_* 三件套基本能覆盖日常需求。界面级的完整流程回归交给 Squish,两者分层配合。

浙公网安备 33010602011771号