☰
QTest 单元测试实战:CMake、信号槽与 GUI 自动化测试
2026/10/31 12:31:45 网站建设 项目流程

简介:面向Qt开发者和初涉单元测试的软件工程师,这份QTest单元测试示例工程聚焦普通应用程序类方法与动态链接库内部非导出类方法的可测试性设计,可作为搭建测试工程、理解Qt测试框架组织方式的实践参考。压缩包共13个文件,以5个cpp源文件、4个h头文件和4个pro工程文件为主,分别承载测试用例实现、被测类声明与qmake工程配置,整体约6KB,体量轻便,便于快速解压运行和按模块研读。资源已有1467人学习下载,说明其在Qt单元测试入门场景中具有一定参考价值。示例覆盖测试类与用例插槽的组织、数据驱动测试与断言宏的使用,并展示通过友元或测试专用导出接口访问DLL内部方法的两种思路,读者可据此理解普通应用与DLL模块的测试边界、工程拆分方式及运行流程,为后续在自有项目中补充单元测试提供可复用的骨架与排错参照。

1. 为什么用 QTest 做单元测试:一个拖到联调才发现的坑

QTest 单元测试 Demo 要解决的是一个很具体的场景:一个 Qt 写的小工具,界面能跑、功能能点、交付时看着没问题,可一旦有人改动核心判断逻辑,比如阈值比较、状态机切换、配置解析,就只能重新手点一遍界面做回归。这种靠人肉点按钮来验证的方式,一次二十分钟起步,还容易漏测边界。这个 Demo 不追求覆盖率数字好看,它把 QTest 做单元测试的四件事串成一条能照搬的链路:测试目标怎么拆、用例怎么写、信号槽与 GUI 交互怎么测、跑出来怎么判断通过。适合正在做 Qt 课设或毕设、手里已经有一个能跑但没测试的 Qt 工程、想补上一套可复用测试骨架的同学;也适合同事接了个内部小工具,需要交付前给核心逻辑兜一层底的人。

2. 测试工程落地:CMake 拆出可测的 core 库与独立测试目标

2.1 构建方式选型:CMake 优先,qmake 保底

Qt 从 5 到 6,官方构建主推 CMake,但课设和不少老工程还在用 .pro。QTest 的接入在两种构建下都能做,差别在于测试用例怎么被"发现"、怎么被一键执行。CMake 侧的关键动作有三个:找到 Test 组件、把被测代码编成独立静态库、用 add_test 把测试可执行文件注册进 CTest。这样 IDE 里的测试面板和命令行 ctest 跑的是同一份用例,不会出现 IDE 里绿、CI 里红这种双标。qmake 侧则要加两行:声明 testlib 依赖、打开 testcase 配置,让 make check 能遍历用例。

先看 CMake 的骨架:

# 顶层 CMakeLists.txt 关键片段 cmake_minimum_required(VERSION 3.16) project(QTestDemo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) # 测试类含 Q_OBJECT,必须开 moc find_package(Qt6 REQUIRED COMPONENTS Core Test) # 被测代码独立成 core 库,不依赖任何 GUI 模块 add_library(core STATIC src/temp_alert_controller.h src/temp_alert_controller.cpp ) target_link_libraries(core PUBLIC Qt6::Core) target_include_directories(core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) enable_testing() add_executable(tst_temp_alert tests/tst_temp_alert.cpp) target_link_libraries(tst_temp_alert PRIVATE core Qt6::Test) add_test(NAME tst_temp_alert COMMAND tst_temp_alert)

这里有两个设计点要交代清楚。一是core库只链 Qt6::Core,绝不链 Qt6::Widgets。一旦链了 Widgets,测试进程启动时会去递归拉起 GUI 依赖,在没有显示环境时直接 abort,这个坑后面第 5 章还会专门讲。二是add_test(NAME tst_temp_alert COMMAND tst_temp_alert)这一行看着不起眼,它是 ctest 认识这个用例的唯一入口,少了它,ctest 一句"0 tests"跑完你还以为全过了。参数上,NAME 就是 ctest 里显示的名字,COMMAND 是测试可执行文件的实际路径,后面接--output-on-failure时失败用例的 stdout 会原样打出来。

提示:CMake 里target_link_libraries的顺序不影响链接结果,但target_include_directories(core PUBLIC ...)决定了测试目标能不能直接#include "temp_alert_controller.h",写 PRIVATE 的话测试目标里就要自己再补一次包含路径。

qmake 场景下等价的两行是这样的:

QT += testlib QT -= gui # 纯逻辑测试不要 GUI 模块 CONFIG += testcase # 让 make check 能遍历本测试目标

QT += testlib引入 QTest 框架本身;CONFIG += testcase会生成 make check 目标,把该 .pro 内的测试函数都挂进去。这两个配置很多同学会漏写第二个,结果用例写完了、编过了,跑起来却什么也没打印。真遇到的时候不用怀疑人生,看 .pro 里这行在不在就行。

2.2 目录结构与源码分离:把业务逻辑抽成 core 库

目录结构直接决定测试是能持续维护,还是一次性用完就废。我的做法是把源码分成三层:可复用的 core 库、编译出的应用/界面、只负责驱动的 tests。落到盘上长这样:

project/ ├── src/ │ ├── temp_alert_controller.h │ ├── temp_alert_controller.cpp │ └── mainwindow.cpp ├── tests/ │ ├── CMakeLists.txt │ ├── tst_temp_alert.cpp │ └── tst_string_utils.cpp └── CMakeLists.txt

抽离的核心原则只有一条:任何"输入值 → 计算 → 输出值"或"输入值 → 改状态 → emit 信号"的逻辑,都往 core 里搬,界面类只负责把控件信号转成对 core 的调用,再把 core 的信号刷回界面。这样测试目标就完全不需要 Widgets,也不依赖真实窗口,整个测试进程可以在纯 headless 环境里跑完。很多 Qt 工程测不下去,不是因为 QTest 难,而是因为业务逻辑长在 QWidget 的槽函数里,想测就得先 new 一个窗口,一 new 窗口测例就开始受"窗口没显示、焦点不在、事件循环没起来"影响,稳不下来。

tests 目录里的 CMakeLists.txt 用循环把每个测试文件注册为一个可执行目标,避免每加一个用例就复制一遍 add_executable:

# tests/CMakeLists.txt set(TEST_SOURCES tst_temp_alert.cpp tst_string_utils.cpp ) foreach(test_src ${TEST_SOURCES}) get_filename_component(test_name ${test_src} NAME_WE) add_executable(${test_name} ${test_src}) target_link_libraries(${test_name} PRIVATE core Qt6::Test) add_test(NAME ${test_name} COMMAND ${test_name}) endforeach()

get_filename_component(... NAME_WE)拿到的是去掉扩展名的文件名,用它同时作为可执行文件名和 CTest 用例名,好处是以后看失败报告时一眼就知道是哪个测试文件挂了,不用再翻 CMake 里的映射。这个循环写法在 20 个以内测试文件规模下很够用,再多就应该上 CTest 的 discover 模式了。

2.3 接入 CTest:让 IDE 和命令行跑同一份用例

配置好之后,真正的运行入口只有一条命令:

cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug cmake --build build -j cd build && ctest --output-on-failure

-DCMAKE_BUILD_TYPE=Debug是必须的,Release 下 QCOMPARE 的失败信息经常被优化掉,报出来的"actual/expected"可能是一堆无意义的内存值,看报告会以为自己写错了断言。-j并行编译只是为了快。--output-on-failure则是让 grep 不进源码的情况下,直接看到失败用例的 stdout 和断言信息,CTest 默认成功时一句话都不吭,没这个参数很难判断到底是真过了还是根本没跑。

如果同时想跑覆盖率,第三步改成这样:

cd build && ctest --output-on-failure && gcovr -r .. --filter src/

-r ..指定源码根目录,--filter src/只统计我们自己的源码行,不然 Qt 头文件和 moc 生成的文件全算进去,覆盖率会虚高到离谱。这个组合在答辩演示里也顺手,屏幕上出现"Ran 12 tests, 100% tests passed"和真实的覆盖率百分比,比任何口头描述都直接。

注意:CTest 的WorkingDirectory默认是构建目录而非源码目录,用例里如果有按相对路径读配置文件的地方,要么改成用QCoreApplication::applicationDirPath()拼绝对路径,要么在add_test里加WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR},否则本地手跑过、ctest 跑就挂。

到这里测试工程骨架就算立住了。下一章开始进入用例本身:一个测试类到底长什么样、断言该用哪个、数据驱动怎么写。

3. 用例设计:生命周期钩子、断言选型与数据驱动表

3.1 测试类骨架:QTEST_MAIN 与四类生命周期钩子

QTest 的测试类必须继承 QObject、带 Q_OBJECT 宏。这个"必须"是语法层面的,不写 Q_OBJECT 就没有 moc 生成,槽函数不会被识别为用例,运行起来零用例也不报错。框架的约定是"私有槽即用例",函数名就是用例名,报告里显示的就是这个名字,所以命名要按测试语义写,不要图省事写成 test1、test2。

主函数由宏生成,分三个档位:QTEST_MAIN生成 QApplication,能测 widget;QTEST_GUILESS_MAIN生成 QCoreApplication,无 GUI 依赖;QTEST_APPLESS_MAIN连 application 对象都不建。纯 core 库测试优先QTEST_GUILESS_MAIN,测 widget 才上QTEST_MAIN。二档选错的典型症状是:要么测试进程起来以后没有QApplication,widget 一 new 就崩;要么明明只测个纯函数,却把整个 GUI 依赖链拖进测试里,跑得慢还受环境影响。

完整骨架长这样,四类生命周期钩子加两条用例:

#include <QtTest> #include "temp_alert_controller.h" class TestTempAlert : public QObject { Q_OBJECT private slots: void initTestCase(); // 整个测试套件开始前跑一次 void cleanupTestCase(); // 整个测试套件结束后跑一次 void init(); // 每个用例前跑一次 void cleanup(); // 每个用例后跑一次 void judge_normalRange(); void judge_boundary_data(); void judge_boundary(); private: TempAlertController *m_ctrl = nullptr; }; void TestTempAlert::initTestCase() { qInfo() << "开始测试 TempAlertController"; } void TestTempAlert::init() { // 每个用例都用新实例,避免状态串味 m_ctrl = new TempAlertController; m_ctrl->configure(10.0, 30.0); } void TestTempAlert::cleanup() { delete m_ctrl; m_ctrl = nullptr; } void TestTempAlert::judge_normalRange() { QCOMPARE(m_ctrl->judge(20.0), TempAlertController::Normal); } QTEST_GUILESS_MAIN(TestTempAlert) #include "tst_temp_alert.moc"

四类钩子里 init / cleanup 最容易混淆。它俩是"每个用例前后各一次"而不是"整个测试跑前后各一次";initTestCase才是整个套件跑前一次。把实例创建放在 init 里,每个用例拿到的都是干净的控制器,不会出现"前一个用例把阈值改成了 50,后一个用例按 30 来测然后莫名挂掉"这种连锁污染。最后的#include "tst_temp_alert.moc"是 moc 生成的副产物,在 .cpp 内定义带 Q_OBJECT 的类时必须手动包含,路径和源文件名一致,写成#include "tst_temp_alert.h"会直接找不到文件。

m_ctrl->configure(10.0, 30.0)里两个参数就是告警下限和上限,下界取 10.0,上界取 30.0,中间的都算正常,措辞上也可以叫"安全区"。这个调用放在 init() 里意味着每个用例的初始阈值一致,改测试预期的时候只需要动这一处。

3.2 断言选择:QVERIFY / QCOMPARE / QVERIFY2 的差别

QTest 提供的断言里最常被混用的是 QVERIFY 和 QCOMPARE。一句话原则:值相等就用 QCOMPARE,条件成立就用 QVERIFY,需要附加解释就用 QVERIFY2。三者差别主要在于失败报告的信息量,而这个信息量直接决定你查问题是 5 分钟还是 1 小时。

断言用于什么失败时打印什么常见误用
QVERIFY(expr)布尔条件成立只打印表达式文本用它比两个值,等于自废报告
QCOMPARE(actual, expected)期望值相等同时打印 actual 和 expected类型不匹配编译报错,要先强转
QVERIFY2(expr, msg)条件 + 自定义说明表达式 + msg每条都写,导致编译期字符串拼接负担大
QEXPECT_FAIL标记已知缺陷标记的失败不算失败滥用会把真问题也盖住

典型误用是QVERIFY(result == TempAlertController::HighAlert)。这个断言失败时你只能看到result == TempAlertController::HighAlert这串源码,却不知道 result 实际是哪个值,得再加一句 qDebug 才知道。换成QCOMPARE(result, TempAlertController::HighAlert)就一次到位。参数上,QCOMPARE 的两个入参类型必须一致或能隐式转换,枚举和 int 之间经常需要显式int(...)转换一下,否则编译期就报 no matching function。

QEXPECT_FAIL 的用法要克制。它本意是"这个用例预期会失败,先挂着",常用于已知缺陷的临时记录。写法是QEXPECT_FAIL("", "已知问题", Continue)加在断言前,之后的那一条断言失败不会让用例判红。项目里我一般在它后面跟一条 TODO 注释,修复时 grep 一把清掉,否则半年后再看,一堆 EXPECT_FAIL 横在测试里,等于把黑匣子封上了后悔药都难救。

3.3 数据驱动:用 QFETCH 与<用例名>_data()替代复制粘贴

边界用例一旦超过三五条,逐个写judge_boundary_01、judge_boundary_02就会变成血泪经验:光函数名就要写十几行,维护起来还得同步改。QTest 提供的数据驱动机制把"输入 + 期望输出 + 行名"放进一张表里,循环遍历跑。

机制只有两条约定:<用例名>_data()是数据供给函数,真正的用例里用 QFETCH 取数据。用起来像这样:

void TestTempAlert::judge_boundary_data() { QTest::addColumn<double>("input"); QTest::addColumn<int>("expected"); // 用 int 承接枚举,方便打印 QTest::newRow("下边界-正常") << 10.0 << int(TempAlertController::Normal); QTest::newRow("下边界-低警") << 9.9 << int(TempAlertController::LowAlert); QTest::newRow("上边界-正常") << 30.0 << int(TempAlertController::Normal); QTest::newRow("上边界-高警") << 30.1 << int(TempAlertController::HighAlert); QTest::newRow("NaN-错误") << std::nan("") << int(TempAlertController::Error); } void TestTempAlert::judge_boundary() { QFETCH(double, input); QFETCH(int, expected); QCOMPARE(int(m_ctrl->judge(input)), expected); }

三个细节值得展开。addColumn的参数是列名和类型,列名不做语义校验但必须和 QFETCH 里用的名字一字不差,大小写也算。newRow("行名")给每一行一个可读标签,用例失败时报告里会打judge_boundary(上边界-高警),比judge_boundary(3)直观得多。QFETCH 里的类型必须和 addColumn 类型精确匹配,double 写成 float、枚举直接写枚举,都会在运行期报 "QFETCH: Type mismatch"。

NaN 那行是刻意留的边界用例,它在写judge()实现时会被逼着处理qIsNaN或提前返回错误。这个用例要是没测出来,接口上的空指针、除零、非法输入就都是沿途埋的雷。把std::nan("")放在数据表里比单开一个用例更省事,也不用担心以后加边界值时忘了它。

int(...)那几处强转看着碍眼,但它是为了绕开 QCOMPARE 对枚举类型支持不够好的问题——枚举两个不同值是 distinct types,直接比在某些 Qt 版本上会编译不过。转 int 之后失败报告里打印的数字能对照第 3.2 节里那张枚举表直接读,反而比打印枚举名更快定位。

4. 信号槽与 GUI 交互测试:QSignalSpy、鼠标键盘与异步等待

4.1 信号槽测试:QSignalSpy 抓信号与参数

很多 Qt 类的"结果"不通过 getter 暴露,而是 emit 一个信号出队,由外部槽决定怎么响应。这类逻辑如果只测 getter 就绕过了整条信号链,测了等于没测。QSignalSpy 就是干这个的:把它挂在某个对象某个 signal 上,它替你把每次发射的实参都收集成 QList ,用count()数次数、at(i).at(j).value<T>()取参数。

void TestTempAlert::alertSignal_emittedOnce() { QSignalSpy spy(m_ctrl, &TempAlertController::alertChanged); m_ctrl->judge(35.0); // 越过上限,应发一次告警 QCOMPARE(spy.count(), 1); QCOMPARE(spy.at(0).at(0).value<TempAlertController::State>(), TempAlertController::HighAlert); }

构造时传对象指针和成员函数指针,这是 Qt5 之后推荐的写法,能编译期校验信号存在。spy.count()等于发射次数,spy.at(i)是第 i 次发射(从 0 开始),spy.at(i).at(j)是这次发射的第 j 个参数,value<T>()转成目标类型。有一点要注意:QSignalSpy 从挂上去的那一刻开始记录,不区分"挂之前的发射"和"挂之后的发射",所以顺序必须是先 spy 再触发,写反了就是"信号明明发了却 count 为 0"这种玄学现象。

还有一种常见需求是"信号确实发了,参数是什么不重要,只要次数对"。这种情况就别把参数取值也写死,QCOMPARE(spy.count(), 1)一条就够。参数细节交给另一个专门测参数的用例,职责清晰,失败报告也好读。

4.2 GUI 交互:mouseClick / keyClick 与不要迷信 qWait

widget 的测试要用QTEST_MAIN,否则 window 起不来。鼠标键盘事件的模拟入口是QTest::mouseClick、QTest::keyClicks、QTest::keyPress这几个。函数签名里 widget 指针、按键、修饰键、点击坐标依次排开,多数场景下用默认坐标就行。

void TestWidget::clickApplyEmitsSignal() { QLineEdit edit; edit.show(); QVERIFY(QTest::qWaitForWindowExposed(&edit)); // 等窗口真正显示出来 QTest::keyClicks(&edit, "hello"); QCOMPARE(edit.text(), QString("hello")); }

qWaitForWindowExposed是这个用例能稳定通过的关键。它做的是"在事件循环里反复检查 widget 是否已经对用户可见",回到 QVERIFY 里就是"没 exposed 就判失败",不会无限等。很多资料里教的是QTest::qWait(200); edit.show();,这种固定等待的问题在于:CI 机器慢的时候 200ms 不够,本地快的时候 200ms 又白等——玄学成分很高。qWaitForWindowExposed 是"等到条件满足或超时"的确定性等待,没这个习惯的测试用例在 CI 上隔三差五红一次,你都不知道该从哪查。

QTest::keyClicks(&edit, "hello")是把 "hello" 逐个字符模拟敲进 QLineEdit,会触发 focus、keyPress、keyRelease、输入法这一整套事件链,比直接调edit.setText("hello")更贴近真实用户操作。代价是它对 widget 的 focus 状态有要求,edit 没显示或没拿到焦点时 keyClicks 是空操作,text() 会返回空串。这就是为什么 show 和 qWaitForWindowExposed 一定得在前面。

注意:QTest::mouseClick 默认按 widget 中心点算坐标;如果控件中心恰好被一个半透明遮罩或 tooltip 挡住,事件会被别的东西吃掉,看起来就是"点了没反应"。这种场景需要显式传 QPoint,避开遮挡区域。

4.3 异步与超时:QTRY_COMPARE 替代手写轮询

只要被测逻辑里带 QTimer、QNetworkAccessManager、QThread 这类异步成分,"立刻断言"就必然挂。手写的"循环 + qWait"很难写对,因为 qWait 本身不处理事件,在有些场景下会把事件队列卡住反而让异步变得更慢。QTest 给出的答案是一组 QTRY_* 宏:QTRY_COMPARE、QTRY_COMPARE_WITH_TIMEOUT、QTRY_VERIFY,它们在超时时间内反复检查条件,每次检查之间都会转一轮事件循环,条件满足立即返回,超时才算失败。

void TestAsync::delayedEmit() { QTimer::singleShot(200, this, [this] { m_value = 42; emit ready(); }); QTRY_COMPARE_WITH_TIMEOUT(m_value, 42, 3000); // 最多等 3 秒 }

默认超时是 5 秒(可用环境变量QTEST_FUNCTION_TIMEOUT改),加_WITH_TIMEOUT后缀可以显式指定本次用例的超时上限,单位毫秒。上面这段里事件在 200ms 后触发,3000ms 的上限留了充足余量,CI 机器慢一点也不会误报。反过来如果上限设成 500ms,机器一抖就红,这个红没有任何信息量,只有噪声。

QTRY_* 宏和 QSignalSpy 的组合非常常见:先 spy 挂上,再触发异步逻辑,最后QTRY_COMPARE(spy.count(), 1)。这个组合同时解决了"信号确实发了"和"发得很晚"两个问题,比"qWait 一下再看 count"要可靠得多——后者在慢机器上永远差一口气。

5. 常见问题排查:四类最容易翻车的 QTest 用例

5.1 现象:用例单独跑能过,整包跑就挂

原因:用例之间共享了静态变量、全局单例或者没清理的临时文件。QTest 在每个测试类内部是顺序执行的,只要前一个用例把某个 static 计数器、配置文件路径、临时目录内容改了,后一个用例读到的就是脏数据。这种失败往往只在一整包跑时出现,单跑时干净得让人怀疑是玄学。

解决:所有可变状态都往 init() / cleanup() 里收。cleanup() 里要做的不是"顺手 delete 一下",而是"恢复到这个用例开始前的世界状态":临时文件删掉、静态变量重置、全局配置复位。如果状态确实跨类共享(比如单例),就在 initTestCase() 里集中断言"这个单例当前是干净的",一旦被污染立刻在第一个用例里暴露出来,而不是等第三个、第四个才莫名失败。

5.2 现象:无显示环境下 GUI 用例"全部通过",其实什么也没测到

原因:CI 或 Docker 环境里没有 X server 或 Wayland,QApplication初始化失败后QTEST_MAIN有时会直接 return 0,CTest 看到退出码是 0 就判通过,于是整个 GUI 测试形同虚设。这类假阳性最坑,因为它会给你一种"测试都过了"的幻觉,等你哪天在本地跑才发现一堆红。

解决:在 CI 上明确加QT_QPA_PLATFORM=offscreen环境变量,让 Qt 用无头后端跑;如果只是 pure core 测试,就切到QTEST_GUILESS_MAIN,从源头上砍掉 GUI 依赖。另外可以在 initTestCase 里加一条QVERIFY(QGuiApplication::platformName().isEmpty() == false),无 platform 直接判失败,让假阳性无法通过。

5.3 现象:覆盖率数字漂亮,分支其实没进去

原因:只看了行覆盖率(line coverage),没看分支覆盖率(branch coverage)。if (a && b && c)这种复合条件,只要走到过一次真分支,行覆盖率就记 100%,但 a、b、c 三个子条件任意一个为假的分支根本没测过。答辩时被问"如果 a 真 b 假呢",答不上来就很尴尬。

解决:gcovr加--branch参数,统计结果里分支覆盖率和行覆盖率并列展示;同时把用例表按"前置条件组合"重新过一遍,尤其是if、?:、&&、||这几类结构,每个子条件各造一个用例。看到分支覆盖率比行覆盖率低一大截,那就是该补用例的地方。

5.4 现象:QTEST_MAIN 编过了,ctest 却找不到用例

原因:三个常见分支。第一,CMake 里漏了add_test,测试程序存在但没注册;第二,qmake 工程里漏了CONFIG += testcase,make check 不会遍历;第三,ctest 默认工作目录是构建目录,测试里用相对路径读文件,运行时直接找不到文件退出,CTest 看到非零退出码却没打印失败信息,看着就像"没找到"。

解决:第一步先cd build && ctest -N,看注册了哪些用例,-N只列不跑;第二步对每个测试可执行文件单独执行./tst_xxx,看退出码和输出;第三步对照 add_test 的 COMMAND 路径是否和实际产物路径一致。三件事排完,绝大多数"找不到用例"都能定位。养成习惯:改完 CMake 先ctest -N过一眼,比等到跑完整包才发现少注册省事。

6. 验收与答辩:一份能当场跑通的测试清单

6.1 一份可复现的验收清单

交付前我会按下面的顺序跑一遍,任何一步不过都当场定位,不带问题交:

# 1. 干净目录重新配置,排除缓存残留 rm -rf build && cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug # 2. 编译 + 跑全部用例 cmake --build build -j && cd build && ctest --output-on-failure # 3. 列出注册用例,确认没有漏注册 ctest -N # 4. 覆盖率 + 分支覆盖率 gcovr -r .. --filter src/ --branch

第 2 步的ctest报告里应该看到 "100% tests passed, 0 tests failed out of N",N 与你实际写的用例数对得上;第 3 步的列表数量和ctest -N输出的编号一致;第 4 步的输出里行覆盖率和分支覆盖率应该都出现。答辩演示时把第 1、2、3 步依次敲一遍,屏幕上的"Ran N tests, 100% tests passed"就是最直接的交付证明,比 PPT 里贴代码截图有说服力得多。

提示:答辩现场的网络和磁盘速度都不稳定,-j并行编译容易在慢机器上卡在某个 moc 生成步骤上。现场演示建议去掉-j,或者降成-j 2,稳比快重要。

6.2 答辩讲解的落脚点

讲测试这一块,最忌讳按"QTest 是什么 → QTest 有哪些断言 → QSignalSpy 能干嘛"这种教程顺序铺开,讲完评委只记住了 API 名字。我会按"这个功能如果不测会出什么事故 → 我测了哪三条输入 → 对应哪几条断言 → 失败长什么样"来组织。举个例子,阈值告警功能,我会先说"如果上下限判断写反,用户会收到错误的告警,这是一个会直接影响功能正确性的问题",再展示 judge_boundary 数据表里五行输入,然后在屏幕上现场改一行断言触发失败,让评委亲眼看到 QCOMPARE 打印出 actual 和 expected 两行。失败报告长什么样,比通过报告更能说明测试有没有真的在测东西。

被追问"覆盖率多少"时,先报行覆盖率,再补一句分支覆盖率,两个数差得多就主动说差在哪。比如"行覆盖 92%、分支覆盖 78%,差的那 14% 集中在 judge() 里的复合条件判断,那部分我按输入组合补了三条数据驱动用例,下个版本会补齐"。这种带自省的答法比硬答"90%"然后被追问穿帮要稳当。

从那以后我每次交 Qt 相关的东西,不管是课设还是内部小工具,都强制先在干净目录重新配一遍再跑 ctest,哪怕只补了三个用例也照做。后悔药不存在,但"干净目录 + ctest -N + 分支覆盖率"这三步能最大限度避免交完才发现问题。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询