qtcanpool 知 09:测试框架

文章目录

前言

很久以前,作者写的代码都没有测试用例,最多就是写个 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 也提供了相应的方法。

相关推荐
云空4 小时前
《QT 5.14.1 搭建 opencv 环境全攻略》
开发语言·qt·opencv
小老鼠不吃猫5 小时前
力学笃行(二)Qt 示例程序运行
开发语言·qt
晓纪同学7 小时前
QT创建一个模板槽和信号刷新UI
开发语言·qt·ui
爱码小白8 小时前
PyQt5 学习方法之悟道
开发语言·qt·学习方法
人才程序员21 小时前
QML z轴(z-order)前后层级
c语言·前端·c++·qt·软件工程·用户界面·界面
学习BigData21 小时前
【使用PyQt5和YOLOv11开发电脑屏幕区域的实时分类GUI】——选择检测区域
qt·yolo·分类
yerennuo1 天前
FFmpeg库之ffmpeg
qt·ffmpeg
冷眼看人间恩怨1 天前
【Qt笔记】QComboBox控件详解
c++·笔记·qt
阿松のblog1 天前
pyQt5实现页面切换操作
开发语言·qt
人才程序员1 天前
【无标题】
c语言·前端·c++·qt·软件工程·qml·界面