文章目录
前言
很久以前,作者写的代码都没有测试用例,最多就是写个 demo 验证一下,毕竟不是专业出身,也没经过大公司的洗礼。
后来,参与到一些项目才知道有专门的测试,而且开发也要测试(开发自测、白盒测试、单元测试)。有兴趣的读者可以去了解一下 TDD(测试驱动开发)方法论。
关于c/c++的测试,作者先后用到过 cpptest、gtest、CMockery。但是,对于 Qt 这种带界面的程序该怎么测试呢?
Qt Test 是一个基于 Qt 的应用程序和库的单元测试框架。Qt Test 提供了单元测试框架中常见的所有功能,以及用于测试图形用户界面的扩展。
Qt Test 旨在简化基于 Qt 的应用程序和库的单元测试编写。
官方介绍:https://doc.qt.io/qt-6/qtest-overview.html
不满
根据 Qt 官方的测试教程,编写一个最简单的测试用例 tst_qstring.cpp 如下所示:
cpp
// tst_qstring.cpp
#include <QtTest/QtTest>
// 测试套
class tst_QString: public QObject
{
Q_OBJECT
private slots:
void toUpper(); // 测试用例
};
void tst_QString::toUpper()
{
QString str = "Hello";
QVERIFY(str.toUpper() == "HELLO");
}
QTEST_MAIN(tst_QString)
#include "tst_qstring.moc"
QTEST_MAIN 宏定义如下,实际上是定义一个 main 函数:
cpp
#define QTEST_MAIN(TestObject) \
int main(int argc, char *argv[]) \
{ \
QTEST_MAIN_IMPL(TestObject) \
}
#define QTEST_MAIN_IMPL(TestObject) \
TESTLIB_SELFCOVERAGE_START(#TestObject) \
QT_PREPEND_NAMESPACE(QTest::Internal::callInitMain)<TestObject>(); \
QApplication app(argc, argv); \
app.setAttribute(Qt::AA_Use96Dpi, true); \
QTEST_DISABLE_KEYPAD_NAVIGATION \
TestObject tc; \
QTEST_SET_MAIN_SOURCE_PATH \
return QTest::qExec(&tc, argc, argv);
作者的不满,不是不满意,而是不满足,具体的不满足是:一个 Qt 测试应用程序只能测试一个测试类,如果需要编写很多测试类,一个个执行测试程序也太麻烦了。
解释如下:一个测试文件中通过 QTEST_MAIN 直接定义了一个 main 函数,通常一个应用程序只能有一个 main 函数,这导致一个测试应用程序只能测试一个测试类,一个测试类通常只测试一个类。
改进
问题描述:一个测试应用程序如何测试多个类?
问题分析:既然 QTEST_MAIN 宏是直接定义一个 main 函数,那么可以参考这个宏,main 函数由用户自己定义,然后在自定义的 main 函数中实例化不同的测试类进行测试,这样就可以在一个 main 函数执行多个测试类,从而实现一个测试应用程序测试多个类。
改进后的代码如下:(由一个 tst_qstring.cpp 文件变成了三个文件:tst_qstring.h、tst_qstring.cpp、main.cpp)
cpp
// tst_qstring.h
#pragma once
#include <QtTest/QtTest>
class tst_QString: public QObject
{
Q_OBJECT
private slots:
void toUpper(); // 测试用例
};
// tst_qstring.cpp
#include "tst_qstring.h"
void tst_QString::toUpper()
{
QString str = "Hello";
QVERIFY(str.toUpper() == "HELLO");
}
// main.cpp
#include <QtTest/QtTest>
#include <QApplication>
// 包含测试套头文件
#include "tst_qstring.h"
#define TEST_RUN(TestObject) \
TestObject TestObject##_var; \
QTest::qExec(&TestObject##_var, argc, argv);
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
// 执行一个测试套
TEST_RUN(tst_QString)
return 0;
}
使用方法:如果想增加其它测试文件,只需要定义对应的 h 和 cpp 文件,然后在 main.cpp 中包含头文件,在 main 函数中调用 TEST_RUN 进行执行。
思考一下:初看没啥问题,实际使用的时候,太麻烦了。每次要多定义一个 h 文件,还要在 main.cpp 中做那么多操作,糟糕的设计。(这个框架思路有点像 cpptest,声明没有借鉴 cpptest)
优化
作者以前先用的 cpptest,后来用了 gtest 后,很爽,只需要调用一个相关宏来定义测试用例,然后 main 函数中调用宏 RUN_ALL_TESTS 就可以运行所有测试用例。
优化思路:
- 定义一个 TEST_ADD 宏,用于在 cpp 文件中取代 QTEST_MAIN。QTEST_MAIN 是直接定义 main 函数,而 TEST_ADD 只是将测试类添加到测试队列中。
- 定义一个 TEST_RUN_ALL 宏,用于在 main 函数中运行所有通过 TEST_ADD 添加的测试用例。
问题的关键点:TEST_ADD 宏该如何实现,才能在 main 函数执行前将测试类添加到测试队列中。因为,main 函数通常是程序的入口函数,可以简单理解为程序运行时执行的第一个函数。
TEST_ADD 宏实现思路:
- 首先,TEST_ADD 宏被先执行,因为宏是在预处理阶段执行。
- 如果 TEST_ADD 宏实现为函数调用,会有问题,因为在函数外面调用一个函数会报编译错误。
- 在某些特定的编译器中,有 constructor 和 destructor 属性,可以用来指定某些函数在程序启动时执行(相当于"构造函数")或在程序退出时执行(相当于"析构函数")。这并不是标准C语言的特性,但在某些编译器下,确实可以用来模拟类似构造函数的效果。简单理解,构造函数可以先于 main 函数执行。但是这特性也不适合 TEST_ADD 宏。
- 利用静态变量,main 函数是在所有静态变量和全局变量初始化完成后执行。(参考了 gtest,但没完全看明白 gtest 的实现)
测试框架实现的部分代码如下:(详细代码:https://gitee.com/icanpool/qtcanpool/tree/master/tests/common)
cpp
///
// tst_global.h
/* UnitTest */
class UnitTest
{
public:
UnitTest();
~UnitTest();
public:
static UnitTest &instance();
void add(QObject *obj);
void run(int argc, char *argv[]);
private:
QVector<QObject *> s_testObjects;
};
/* TestInfo */
class TestInfo
{
public:
TestInfo(QObject *obj);
};
#define TEST_ADD(TestObject) \
class TestObject##_TestClass \
{ \
public: \
static TestInfo _info; \
}; \
TestInfo TestObject##_TestClass::_info = TestInfo(new TestObject);
#define TEST_RUN_ALL() UnitTest::instance().run(argc, argv);
///
// tst_global.cpp
UnitTest &UnitTest::instance()
{
static UnitTest s_testInstance;
return s_testInstance;
}
void UnitTest::add(QObject *obj)
{
if (obj) {
s_testObjects.append(obj);
}
}
void UnitTest::run(int argc, char *argv[])
{
for (QObject *obj : s_testObjects) {
QTest::qExec(obj, argc, argv);
}
qDeleteAll(s_testObjects);
s_testObjects.clear();
}
TestInfo::TestInfo(QObject *obj)
{
UnitTest::instance().add(obj);
}
///
// main.cpp
#include <QApplication>
#include "tst_global.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
TEST_RUN_ALL();
return 0;
}
使用方法:
- 按 Qt 官方的教程编写测试文件,然后用 TEST_ADD 取代 QTEST_MAIN。
- 编写 main.cpp 文件,在 main 函数中调用 TEST_RUN_ALL 宏函数。
具体使用,可以参考 qtcanpool 中的测试代码:https://gitee.com/icanpool/qtcanpool/tree/master/tests
后语
经过一段时间的测试,作者简单的体会是:Qt 虽然写的是界面程序,但本质是 C++,只要按照类来理解就方便测试了,主要测试类的接口。界面程序相比后台程序多了一些鼠标和按键的测试,这部分 QTest 也提供了相应的方法。