之前的文章介绍了如何使用QTest进行单元测试,其实QT的测试框架功能不仅于此,我们还可以通过QTest实现一些更加复杂的测试,这里就介绍一下如何通过QTest实现数据集测试、性能测试、GUI测试。
对于很多算法模块和业务逻辑模块,测试用例的逻辑是相同的,区别只是输入的数据不同。为了重复利用逻辑模块,避免大量的冗余测试代码,我们可以采用QTest提供的数据集模块来对对应的程序进行测试。一个数据集是一个二维的数据表,表的每一行都是一个测试,表中包含了测试的名称、测试的输入数据、以及预期的输出结果等。测试表结构如下所示:
| index | name | number1 | number2 | result |
|---|---|---|---|---|
| 0 | two_positive | 655 | 655 | 1310 |
| 1 | two_negative | -60 | -60 | -120 |
| 2 | zero_negative | 0 | -60 | -60 |
| 3 | zero_positive | 0 | 80 | 80 |
| 4 | zero_zero | 0 | 0 | 0 |
| 5 | positive_negative | -60 | 80 | 20 |
在测试用例类中所有的private槽函数都会被视为测试用例,我们添加如下的测试用例:
class MyPlus
{
public:
MyPlus() = default;
~MyPlus() = default;
int calculate_tow_number(int number1, int number2)
{
return number1 + number2;
}
};
class CustomTest : public QObject
{
...
private slots:
//构造testPlus的数据集
void testPlus_data();
//testPlus测试
void testPlus();
...
};
#endif // CUSTOMTEST_H
testPlus_data()函数负责给测试用例testPlus构造数据集,对应测试用例的数据集构造函数的名称都是:用例函数名称_data。比如你的测试函数名称是A()那么对应的数据集构造函数名称就是A_data()。函数对应的实现如下:
void CustomTest::testPlus_data()
{
QTest::addColumn<int>("number1");
QTest::addColumn<int>("number2");
QTest::addColumn<int>("result");
QTest::newRow("two_positive") << 655 << 655 << 1310;
QTest::newRow("two_negative") << -60 << -60 << -120;
QTest::newRow("zero_negative") << 0 << -60 << -60;
QTest::newRow("zero_positive") << 0 << 80 << 80;
QTest::newRow("zero_zero") << 0 << 0 << 0;
QTest::newRow("positive_negative") << -60 << 80 << 20;
}
void CustomTest::testPlus()
{
QFETCH(int, number1);
QFETCH(int, number2);
QFETCH(int, result);
MyPlus plus;
int ret = plus.calculate_tow_number(number1,number2);
QCOMPARE(ret, result);
}
首先我们通过QTest::addColumn向数据表中添加列并指定每一列的名称和对应的数据类型。指定了列之后,我们就可以通过QTest::newRow,向对应的数据表中添加测试数据了。需要注意的是每一个测试用例的名称,是测试框架使用的不是测试用例使用,所以在创建表的时候不需要单独指定对应的列。
在测试用例中,我们可以通过QFETCH宏依据列名称和类型将其中的数据取出来进行测试。框架会自动按照添加顺序,依次取出数据进行测试并自动完成这个迭代过程。测试用例的输出结果如下所示:
PASS : CustomTest::initTestCase()
PASS : CustomTest::testPlus(two_positive)
PASS : CustomTest::testPlus(two_negative)
PASS : CustomTest::testPlus(zero_negative)
PASS : CustomTest::testPlus(zero_positive)
PASS : CustomTest::testPlus(zero_zero)
PASS : CustomTest::testPlus(positive_negative)
对于一些算法模块,我们需要测试其在不同的数据规模下性能是否保持一致,会不会出现性能衰减。这就需要使用框架测试对应的模块在不同的数据规模下的执行速度怎么样,是不是呈线性关系。下面以一个简单的排序算法为例说明一下性能测试的用法:
class NumberSort
{
public:
NumberSort() = default;
~NumberSort() = default;
void sort_number(QList<int> number_list)
{
qSort(number_list.begin(),number_list.end());
}
};
class CustomTest : public QObject
{
...
private slots:
//性能测试构造数据集
void perform_test_data();
//性能测试
void perform_test();
...
};
#endif // CUSTOMTEST_H
性能测试,我们也需要根据业务构造对应的数据集。
void CustomTest::perform_test_data()
{
QTest::addColumn<int>("loopCount");
QTest::newRow("loopCount: 10") << 10;
QTest::newRow("loopCount: 100") << 100;
QTest::newRow("loopCount: 1000") << 1000;
QTest::newRow("loopCount: 10000") << 10000;
QTest::newRow("loopCount: 100000") << 100000;
}
//测试在不同的规模下测试排序算法的性能
void CustomTest::perform_test()
{
QFETCH(int,loopCount);
qsrand(QDateTime::currentDateTime().toTime_t());
QList<int> number_list;
for(int index=0; index<loopCount; ++index)
{
number_list << qrand() % 10000;
}
NumberSort sort;
QBENCHMARK {
sort.sort_number(number_list);
}
}
对应的执行结果如下所示:
PASS : CustomTest::perform_test(loopCount: 10)
RESULT : CustomTest::perform_test():"loopCount: 10":
0.0013 msecs per iteration (total: 87, iterations: 65536)
PASS : CustomTest::perform_test(loopCount: 100)
RESULT : CustomTest::perform_test():"loopCount: 100":
0.018 msecs per iteration (total: 74, iterations: 4096)
PASS : CustomTest::perform_test(loopCount: 1000)
RESULT : CustomTest::perform_test():"loopCount: 1000":
0.24 msecs per iteration (total: 63, iterations: 256)
PASS : CustomTest::perform_test(loopCount: 10000)
RESULT : CustomTest::perform_test():"loopCount: 10000":
3.1 msecs per iteration (total: 51, iterations: 16)
PASS : CustomTest::perform_test(loopCount: 100000)
RESULT : CustomTest::perform_test():"loopCount: 100000":
39 msecs per iteration (total: 78, iterations: 2)
PASS : CustomTest::cleanupTestCase()
QBENCHMARK宏会自动对函数的指定过程进行测试。有一点需要注意,由于每个测试的单次运行时间偏差比较大,为了防止测试结果偏差,QBENCHMARK会自动指定一个重复次数,取重复运行事件的平均值。这个重复次数跟测试用例单次运行的时间有关系。
对于QT程序的GUI测试,主要流程其实就是模拟鼠标键盘操作对应的控件,然后查看控件状态和属性是否发生了变化还有其成员变量的属性状态是否发生了变化。以下面的一个界面作为测试数据进行测试:
//mywidget.h
#ifndef MYWIDGET_H
#define MYWIDGET_H
#include <QWidget>
#include <QPushButton>
namespace Ui {
class MyWidget;
}
class MyWidget : public QWidget
{
Q_OBJECT
public:
explicit MyWidget(QWidget *parent = 0);
~MyWidget();
bool eventFilter(QObject* watched, QEvent* event) override;
protected:
void mousePressEvent(QMouseEvent *event);
private:
Ui::MyWidget *ui;
};
#endif // MYWIDGET_H
//mywidget.cpp
#include "mywidget.h"
#include "ui_mywidget.h"
#include <QPushButton>
#include <QMouseEvent>
MyWidget::MyWidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::MyWidget)
{
ui->setupUi(this);
installEventFilter(this);
connect(ui->start_btn,&QPushButton::clicked,this,[&](){
ui->start_btn->setEnabled(false);
ui->stop_btn->setEnabled(true);
ui->reset_btn->setEnabled(true);
});
}
MyWidget::~MyWidget()
{
delete ui;
}
void MyWidget::mousePressEvent(QMouseEvent *event)
{
if(event->button() == Qt::RightButton)
{
if(!ui->start_btn->isEnabled())
{
ui->start_btn->setEnabled(true);
}
}
}
bool MyWidget::eventFilter(QObject* watched, QEvent* event)
{
if (event->type() == QEvent::KeyPress)
{
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
Qt::KeyboardModifiers modifier = keyEvent->modifiers();
if(keyEvent->key() == Qt::Key_S && modifier == Qt::NoModifier)
{
ui->start_btn->setEnabled(true);
}
else if(keyEvent->key() == Qt::Key_T && modifier == Qt::NoModifier)
{
ui->stop_btn->setEnabled(true);
}
else if(keyEvent->key() == Qt::Key_R && modifier == Qt::NoModifier)
{
ui->reset_btn->setEnabled(true);
}
else if(keyEvent->key() == Qt::Key_P && modifier == Qt::ControlModifier)
{
ui->reset_btn->setEnabled(false);
ui->start_btn->setEnabled(false);
ui->stop_btn->setEnabled(false);
}
}
return QObject::eventFilter(watched, event);
}
对应的界面效果如下:

界面中主要包含三个按钮start_btn,stop_btn和reset_btn,按下start_btn之后可以影响其它按钮的状态,同时我们还可以通过鼠标事件和键盘事件来改变这三个按钮的状态。
测试数据搭建完毕之后,我们就可以通过模拟鼠标键盘事件对对应的界面进行测试了,测试用例实现如下所示:
//TestGui.h
#ifndef TESTGUI_H
#define TESTGUI_H
#include <QTest>
#include "mywidget.h"
class TestGui : public QObject
{
Q_OBJECT
public:
TestGui(QObject* parent = nullptr);
private slots:
//模拟鼠标事件进行测试
void simulateMouseClick();
//模拟键盘事件进行测试
void simulateKeyPress();
private:
MyWidget mMainWindow;
};
#endif // TESTGUI_H
//TestGui.cpp
#include "TestGui.h"
#include <QPushButton>
#include <QtTest/QtTest>
#include <qtestmouse.h>
TestGui::TestGui(QObject* parent) :
QObject(parent),
mMainWindow()
{
//1秒之后进入事件循环,防止界面没有初始化完成
QTestEventLoop::instance().enterLoop(1);
}
void TestGui::simulateKeyPress()
{
//根据ObjectName查找对应的控件
QPushButton* startButton = mMainWindow.findChild<QPushButton*>("start_btn");
QPushButton* stopButton = mMainWindow.findChild<QPushButton*>("stop_btn");
QPushButton* resetButton = mMainWindow.findChild<QPushButton*>("reset_btn");
startButton->setEnabled(false);
stopButton->setEnabled(false);
resetButton->setEnabled(false);
//模拟按下单个按键
QTest::keyClick(&mMainWindow,Qt::Key_S);
QCOMPARE(startButton->isEnabled(), true);
QTest::keyClick(&mMainWindow,Qt::Key_T);
QCOMPARE(stopButton->isEnabled(), true);
QTest::keyClick(&mMainWindow,Qt::Key_R);
QCOMPARE(resetButton->isEnabled(), true);
//模拟按下组合键
QTest::keyClick(&mMainWindow,Qt::Key_P,Qt::ControlModifier);
QCOMPARE(resetButton->isEnabled(), false);
QCOMPARE(startButton->isEnabled(), false);
QCOMPARE(stopButton->isEnabled(), false);
}
void TestGui::simulateMouseClick()
{
//模拟鼠标点击
QPushButton* startButton = mMainWindow.findChild<QPushButton*>("start_btn");
QPushButton* stopButton = mMainWindow.findChild<QPushButton*>("stop_btn");
QPushButton* resetButton = mMainWindow.findChild<QPushButton*>("reset_btn");
//鼠标左键点击
QTest::mouseClick(startButton, Qt::LeftButton);
QCOMPARE(stopButton->isEnabled(), true);
QCOMPARE(startButton->isEnabled(), false);
QCOMPARE(resetButton->isEnabled(), true);
//鼠标右键点击
QTest::mouseClick(&mMainWindow,Qt::RightButton);
QCOMPARE(startButton->isEnabled(),true);
//鼠标中键
QTest::mouseClick(&mMainWindow,Qt::MiddleButton);
//模拟鼠标双击
QTest::mouseDClick(&mMainWindow,Qt::LeftButton);
//模拟组合点击
QTest::mouseClick(&mMainWindow,Qt::LeftButton,Qt::ControlModifier);
}
测试的输出结果如下所示:
PASS : TestGui::initTestCase()
PASS : TestGui::simulateMouseClick()
PASS : TestGui::simulateKeyPress()
PASS : TestGui::cleanupTestCase()
测试工程中一般会有多个测试类,很多时候我们不需要把所有的测试都跑一遍,这时候我们就可以通过命令行参数来指定需要执行的测试了。同时我们也可以通过命令行参数来对测试框架进行配置,从而将测试结果以不同的格式输出,对应的实现如下:
#include <map>
#include <QCoreApplication>
#include <QTest>
#include <memory>
#include "customtest.h"
#include "TestGui.h"
using namespace std;
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QStringList arguments = QCoreApplication::arguments();
map<QString, unique_ptr<QObject>> tests;
//需要C++14支持
tests.emplace("customeTest", std::make_unique<CustomTest>());
tests.emplace("guiTest", std::make_unique<TestGui>());
//根据命令行参数执行对应的测试
if (arguments.size() >= 3 && arguments[1] == "-select") {
QString testName = arguments[2];
auto iter = tests.begin();
while(iter != tests.end()) {
if (iter->first != testName) {
iter = tests.erase(iter);
} else {
++iter;
}
}
arguments.removeOne("-select");
arguments.removeOne(testName);
}
int status = 0;
for(auto& test : tests) {
status |= QTest::qExec(test.second.get(), arguments);
}
return status;
}
通过组合使用单元测试、数据集测试、性能测试和GUI测试,我们的测试就可以覆盖更多的软件使用场景,从而提升系统的鲁棒性,将很多问题扼杀在摇篮之中,防止其对系统产生破坏。