在第六章中,课程与作业管理系统已经成为可以完整演示的 MVP。本章停止新增业务功能,通过 Qt Test、模型测试、异常场景和性能检查,把“能够运行”推进为“可以稳定交付”。
测试不是在项目完成后随便点几下。它需要明确输入、预期结果和失败证据,并且能够在每次修改后重复执行。
1. 本章目标
完成本章后,应当能够:
- 区分单元测试、集成测试和界面流程测试;
- 使用 Qt Test 与 CTest;
- 编写数据驱动测试;
- 使用 QSignalSpy 验证信号;
- 使用 QAbstractItemModelTester 检查模型约定;
- 为 SQLite Repository 建立隔离测试库;
- 建立异常场景测试矩阵;
- 使用日志、断点和最小复现定位缺陷;
- 形成可发布候选版本。
2. 测试层次
| 层次 | 测试对象 | 特点 |
|---|---|---|
| 单元测试 | Validator、Service、Model | 快速、定位明确 |
| 集成测试 | Repository 与 SQLite | 验证真实模块协作 |
| 界面流程测试 | 新增、搜索、导入、重启 | 接近用户操作 |
| 部署测试 | 打包后的独立目录 | 检查 DLL、插件和路径 |
课程项目不需要追求一个漂亮但没有意义的覆盖率数字。优先覆盖业务规则、数据写入、模型边界和最容易出错的异常路径。
3. 在 CMake 中加入 Qt Test
将可测试的业务代码放进独立库,界面程序和测试程序共同链接它:
find_package(Qt6 REQUIRED COMPONENTS
Core
Widgets
Sql
Test
)
add_library(course_core STATIC
src/domain/assignment.h
src/services/assignmentvalidator.cpp
src/services/assignmentvalidator.h
src/services/assignmentservice.cpp
src/services/assignmentservice.h
src/data/assignmentrepository.cpp
src/data/assignmentrepository.h
src/models/assignmenttablemodel.cpp
src/models/assignmenttablemodel.h
)
target_link_libraries(course_core PUBLIC
Qt6::Core
Qt6::Sql
)
target_include_directories(course_core PUBLIC src)
target_link_libraries(course_manager PRIVATE
course_core
Qt6::Widgets
)
include(CTest)
if(BUILD_TESTING)
qt_add_executable(test_assignment_validator
tests/tst_assignmentvalidator.cpp
)
target_link_libraries(test_assignment_validator PRIVATE
course_core
Qt6::Test
)
add_test(
NAME assignment_validator
COMMAND test_assignment_validator
)
endif()
配置、构建和运行测试:
cmake -S . -B build -DBUILD_TESTING=ON
cmake --build build --config Debug
ctest --test-dir build -C Debug --output-on-failure
多配置生成器需要通过 -C Debug 告诉 CTest 使用哪个配置。
4. 数据驱动的输入验证测试
Qt Test 可以把多组输入与同一段测试逻辑分开:
#include <QtTest>
#include "services/assignmentvalidator.h"
class TestAssignmentValidator final : public QObject {
Q_OBJECT
private slots:
void validate_data();
void validate();
};
void TestAssignmentValidator::validate_data() {
QTest::addColumn<QString>("title");
QTest::addColumn<QDate>("deadline");
QTest::addColumn<bool>("expectedValid");
QTest::newRow("valid")
<< QStringLiteral("完成数据库作业")
<< QDate(2026, 9, 20)
<< true;
QTest::newRow("blank-title")
<< QStringLiteral(" ")
<< QDate(2026, 9, 20)
<< false;
QTest::newRow("invalid-date")
<< QStringLiteral("完成数据库作业")
<< QDate()
<< false;
}
void TestAssignmentValidator::validate() {
QFETCH(QString, title);
QFETCH(QDate, deadline);
QFETCH(bool, expectedValid);
const QString error =
AssignmentValidator::validate(title, deadline);
QCOMPARE(error.isEmpty(), expectedValid);
}
QTEST_APPLESS_MAIN(TestAssignmentValidator)
#include "tst_assignmentvalidator.moc"
测试名称应描述场景,而不是写成 test1、test2。测试失败时,名称本身就是定位线索。
5. 使用 QSignalSpy 验证状态通知
如果 Service 保存成功后应发出 assignmentsChanged,可以这样验证:
QSignalSpy changedSpy(
&service,
&AssignmentService::assignmentsChanged);
const OperationResult result = service.createAssignment(
courseId,
QStringLiteral("复习 Qt Model/View"),
QDate(2026, 9, 25));
QVERIFY(result.success);
QCOMPARE(changedSpy.count(), 1);
还要测试失败时不应发出信号:
const OperationResult result = service.createAssignment(
courseId,
QStringLiteral(" "),
QDate());
QVERIFY(!result.success);
QCOMPARE(changedSpy.count(), 0);
测试不仅检查返回值,还要检查可观察副作用是否准确发生一次。
6. 使用 QAbstractItemModelTester
自定义模型需要满足大量索引和通知约定。Qt Test 提供模型测试器:
#include <QAbstractItemModelTester>
AssignmentTableModel model;
QAbstractItemModelTester tester(
&model,
QAbstractItemModelTester::FailureReportingMode::QtTest);
model.addAssignment({
1,
QStringLiteral("数据库原理"),
QStringLiteral("事务恢复实验"),
QDate(2026, 9, 30),
false
});
QCOMPARE(model.rowCount(), 1);
QCOMPARE(
model.index(0, AssignmentTableModel::TitleColumn)
.data(Qt::DisplayRole).toString(),
QStringLiteral("事务恢复实验"));
如果 beginInsertRows()、父索引或行列范围错误,模型测试器能够提供比“表格偶尔崩溃”更直接的证据。
7. 隔离 SQLite 集成测试
测试不应访问真实用户数据库。可以使用 SQLite 内存数据库,并为每个测试连接指定唯一名称:
const QString connectionName =
QStringLiteral("test-%1")
.arg(QUuid::createUuid().toString(QUuid::WithoutBraces));
QSqlDatabase database = QSqlDatabase::addDatabase(
"QSQLITE", connectionName);
database.setDatabaseName(":memory:");
QVERIFY(database.open());
测试内容至少包括:
- 首次创建表;
- 新增并查询;
- 更新完成状态;
- 删除作业;
- 重复课程或作业被拒绝;
- 外键删除行为;
- 事务失败后回滚。
清理连接时,要先销毁所有使用它的 QSqlQuery 和 QSqlDatabase 副本,再调用:
database.close();
database = {};
QSqlDatabase::removeDatabase(connectionName);
否则 Qt 会提示连接仍在使用。
8. 手工界面测试矩阵
部分交互仍适合通过明确的人工用例验证:
| 编号 | 场景 | 操作 | 预期结果 |
|---|---|---|---|
| UI-01 | 空数据启动 | 删除测试库后启动 | 显示空状态和新增入口 |
| UI-02 | 新增作业 | 输入合法内容并确定 | 列表与统计同步更新 |
| UI-03 | 空标题 | 只输入空格并确定 | 阻止保存并定位输入框 |
| UI-04 | 取消新增 | 输入后点击取消 | 数据库不产生新记录 |
| UI-05 | 搜索 | 输入课程或标题关键字 | 只显示匹配记录 |
| UI-06 | 排序后删除 | 排序并删除选中行 | 删除正确数据库记录 |
| UI-07 | 重启恢复 | 修改数据后重启 | 数据和设置保持一致 |
| UI-08 | 导入损坏 JSON | 选择无效文件 | 提示错误且数据库不变 |
每次修复缺陷后,把能够复现它的步骤加入测试矩阵,防止同类问题再次出现。
9. 异常场景清单
除了正常路径,还要主动测试:
- 应用数据目录不可写;
- 数据库文件被占用或损坏;
- SQLite 驱动缺失;
- 配置值类型或范围错误;
- JSON 为空、截断或字段缺失;
- 导入数据重复;
- 文件选择被取消;
- 网络离线、超时或返回无效 JSON;
- 工作线程失败或用户取消;
- 模型为空时执行编辑和删除;
- 用户连续快速点击同一操作。
测试异常时不仅看程序是否崩溃,还要检查数据库有没有被部分修改、界面能否恢复操作、错误信息是否清楚。
10. 日志与调试流程
遇到问题时使用固定流程:
- 写出最短复现步骤;
- 记录输入、环境和实际结果;
- 查看日志中的第一个异常;
- 在业务入口和失败分支设置断点;
- 检查模型索引、数据库 ID 和线程归属;
- 缩小到最少对象和最少数据;
- 修复后补充自动或人工测试;
- 运行完整测试集确认没有回归。
不要同时修改多个可能原因。一次只验证一个假设,才能知道是哪项改动真正解决了问题。
11. 性能与响应性检查
课程项目的常见性能问题并不是算法极限,而是:
- 每次输入一个字符就查询整个数据库;
- 同一个变化触发多次全量刷新;
- 在主线程读取巨大文件;
- 模型更新时错误地反复 reset;
- 表格一次加载远超实际需要的数据;
- 日志在循环中输出大量内容。
可以使用 QElapsedTimer 测量关键操作:
QElapsedTimer timer;
timer.start();
const QVector<Assignment> items = repository.all();
qCInfo(lcAssignment)
<< "Loaded" << items.size()
<< "assignments in" << timer.elapsed() << "ms";
先测量再优化。不要为了理论上的性能牺牲清晰架构,也不要把短暂的简单查询随意移到线程。
12. 发布候选版本标准
可以为准备发布的版本打标签,例如 v0.9.0-rc1。它应该满足:
- Must 功能全部完成;
- 自动测试全部通过;
- 手工主流程测试通过;
- 没有已知崩溃和数据损坏问题;
- 没有未处理的编译警告;
- Debug 和 Release 都能构建;
- 默认数据与真实个人数据已经分离;
- 日志不包含敏感信息;
- 版本号、README 和变更记录一致。
发布候选阶段只修复缺陷,不继续调整大范围架构或加入新模块。
13. 七个练习步骤
| 顺序 | 学习与实践 |
|---|---|
| 第 1 步 | 把可测试业务代码提取为 course_core |
| 第 2 步 | 配置 Qt Test、CTest 和测试目标 |
| 第 3 步 | 为输入验证编写数据驱动测试 |
| 第 4 步 | 用 QSignalSpy 测试 Service 信号 |
| 第 5 步 | 用 QAbstractItemModelTester 检查模型 |
| 第 6 步 | 用内存 SQLite 测试 Repository 和事务 |
| 第 7 步 | 完成异常矩阵并生成发布候选版本 |
14. 常见问题
测试只能在 Qt Creator 中运行
确认测试已经通过 add_test() 注册,并使用 CTest 从构建目录执行。可重复的命令行测试更适合最终验收。
测试修改了真实数据库
Repository 不应在内部硬编码生产路径。测试时注入命名的内存连接或临时数据库文件。
QSignalSpy 一直为零
检查信号是否在成功分支发出、对象是否为同一实例,以及操作是否因为验证失败提前返回。
模型测试器报告行数错误
检查 beginInsertRows() 和 beginRemoveRows() 的范围,以及数据容器修改发生在 begin 与 end 之间。
手工测试每次结果不同
固定初始数据、操作步骤和预期结果。测试开始前明确恢复干净数据库还是使用迁移后的旧数据库。
15. 本章验收清单
- 业务代码可以脱离 MainWindow 测试;
- CTest 能发现并执行全部测试;
- Validator 有正常和边界输入测试;
- Service 的成功与失败信号数量正确;
- 自定义 Model 通过模型测试器检查;
- Repository 使用隔离 SQLite 测试;
- 事务回滚经过验证;
- 手工测试矩阵记录了实际结果;
- 空数据、损坏文件和取消操作均已测试;
- 主线程没有明显长时间冻结;
- Release 构建没有新增警告;
- 发布候选版本可以稳定完成演示流程。
下一章将把发布候选版本构建成独立目录,检查 Qt DLL、平台插件和 SQLite 驱动,并准备最终文档与答辩演示。
官方参考:Qt Test | Qt Test 概览 | QAbstractItemModelTester
上一篇:第 6 章:功能整合与 MVP | 下一篇:第 8 章:Release、部署与课程答辩