QT项目搭建完整的单元测试流程
在介绍QT的单元测试框架之前,先说一下单元测试。单元测试最重要的就是要将应用拆分成一个个独立的可测试的函数模块。只有将应用拆分成一个个函数模块之后,应用才是可测的。所以开发领域衍生出来了一个概念,Test-driven development(TDD)测试驱动的开发。将应用拆分成一个个独立的可测试的模块之后,我们就可以针对函数模块进行测试编码了。
针对函数模块的各种可能的调用场景编写测试用例,这样每次我们的代码修改的时候,我们都可以通过测试来验证我们的修改是否会对模块的功能产生影响。这样就相当于给我们的软件添加了一层防护网。编写测试用例很早之前都得是靠自己手写的,这样效率很低,后来出现了一些测试框架能帮助开发者自动化完成这一工作,比较出名的测试框架有Google Test和我们今天介绍的Qt Test。
使用Qt的测试框架,首先新建一个测试工程
新建完测试工程之后,我们在工程中添加所有单元测试用例用到的基类,基类中包含了所有测试用例用到的通用方法和属性。
//test-suite.h
#ifndef TESTSUITE_H
#define TESTSUITE_H
#include <QObject>
#include <QString>
#include <QtTest/QtTest>
#include <vector>
namespace test {
class TestSuite : public QObject
{
Q_OBJECT
public:
explicit TestSuite(const QString& _testName = "");
virtual ~TestSuite();
//测试用例的名称
QString testName;
//获取所有实例化的测试用例
static std::vector<TestSuite*>& testList();
};
}
#endif
//test-suite.cpp
#include "test-suite.h"
#include <QDebug>
namespace test {
TestSuite::TestSuite(const QString& _testName)
: QObject()
, testName(_testName)
{
//只要测试用例创建就会被添加到静态容器中
qDebug() << "create test:" << testName;
testList().push_back(this);
qDebug() << testList().size() << "test added";
}
TestSuite::~TestSuite()
{
qDebug() << "destory test:" << testName;
}
std::vector<TestSuite*>& TestSuite::testList()
{
static std::vector<TestSuite*> instance = std::vector<TestSuite*>();
return instance;
}
}
通过基类的方法和成员变量,我们就可以获取每个测试用例的名称,以及所有实例化的测试用例的列表了。默认的测试工程是没有添加程序的入口函数的,需要手动添加,程序的入口函数如下,在程序入口中我们执行所有的测试用例,并将测试结果输出到xml文件中,为后面的分析和使用做准备。
//main.cpp
#include <QtTest/QtTest>
#include <QDebug>
#include "test-suite.h"
#include "demotest.h"
using namespace test;
int main(int argc, char *argv[])
{
Q_UNUSED(argc);
Q_UNUSED(argv);
qDebug() << "found:" << TestSuite::testList().size() << "test suite";
//记录测试失败的数量
int failedTestsCount = 0;
for(TestSuite* i : TestSuite::testList())
{
qDebug() << "Executing test " << i->testName;
//执行每个测试
QString filename(i->testName + ".xml");
//执行每个测试并将测试结果输出到xml文件中
int result = QTest::qExec(i, QStringList() << " " << "-o" << filename << "-xunitxml");
qDebug() << "Test result " << result;
if(result != 0)
{
failedTestsCount++;
}
}
qDebug() << "Test suite complete:" << failedTestsCount << "failures detected.";
return failedTestsCount;
}
QTest::qExec支持很多配置参数,这里选几个常用的介绍一下,有需要更加详细的可以去查询官方文档。
-o filename format 以指定的格式将测试结果输出到某个文件
-o filename 将测试结果输出到某个文件
-txt 以纯文本的形式输出测试信息
-xml 以XML的形式输出测试信息
-lightxml 以轻量级xml的形式输出测试信息
-xunitxml 将测试结果输出成xml文档
-csv 以CSV的格式输出测试信息
-teamcity 以TeamCity格式输出结果
搭建完成测试用例执行的框架之后,我们就可以为每一个单元模块添加测试了,这里以一个简单的模块为例介绍一下测试用例的用法。
这个模块主要有两个方面的功能
1.求两个整数的和
2.求时间的各种表示格式
//demo.h
#ifndef DEMO_H
#define DEMO_H
#include <QObject>
#include <QDateTime>
class Demo : public QObject
{
Q_OBJECT
public:
Demo(const QDateTime& time);
Demo();
public:
int addTwoNumber(int number1,int number2);
QString toIso8601String() const;
QString toPrettyDateString() const;
QString toPrettyTimeString() const;
QString toPrettyString() const;
private:
QDateTime datetime;
signals:
void inputtwozero();
};
#endif // DEMO_H
//demo.cpp
#include "demo.h"
#include <QLocale>
Demo::Demo(const QDateTime &time):
QObject(),
datetime(time)
{}
Demo::Demo()
{
}
int Demo::addTwoNumber(int number1, int number2)
{
if((number1 == 0) && (number2 == 0))
{
emit inputtwozero();
}
return number1 + number2;
}
QString Demo::toIso8601String() const
{
if (datetime.isNull()) {
return "";
} else {
return datetime.toString(Qt::ISODate);
}
}
QString Demo::toPrettyString() const
{
if (datetime.isNull()) {
return "Not set";
} else {
QLocale locale = QLocale::English;
return locale.toString(datetime,"ddd d MMM yyyy @ HH:mm:ss");
}
}
QString Demo::toPrettyDateString() const
{
if (datetime.isNull()) {
return "Not set";
} else {
QLocale locale = QLocale::English;
return locale.toString(datetime,"d MMM yyyy");
}
}
QString Demo::toPrettyTimeString() const
{
if (datetime.isNull()) {
return "Not set";
} else {
QLocale locale = QLocale::English;
return locale.toString(datetime,"hh:mm ap");
}
}
在测试工程中为单元模块添加测试用例,测试用例添加有几点需要了解。
1.测试用例的初始化函数和清理函数
测试用例中有几个被预留的函数,负责初始化测试用例以及测试用例执行完之后的清理操作。他们分别是:
/// 在第一个测试函数执行之前被调用的初始化函数
void initTestCase();
/// 在最后一个测试函数执行完毕之后调用的清理操作
void cleanupTestCase();
/// 每次执行测试函数之前调用的初始化函数
void init();
/// 每次执行完测试函数之后调用的清理操作
void cleanup();
2.记录某个信号触发的次数
在测试工程中,有时候我们需要验证某个信号是否触发,触发了多少次,这时候我们需要使用QSignalSpy类来记录信号触发的次数。
3.在测试用例的实现文件中添加静态的全局变量,这样在构建工程的时候测试用例就会自动添加到测试用例列表。
测试用例的实现如下:
//demotest.h
#ifndef DEMOTEST_H
#define DEMOTEST_H
#include <QtTest/QtTest>
#include "test-suite.h"
namespace test {
class DemoTest : public TestSuite
{
Q_OBJECT
public:
DemoTest();
private slots:
/// @brief 第一个测试函数执行之前被调用
void initTestCase();
/// @brief 最后一个测试函数执行之后被调用
void cleanupTestCase();
/// @brief 每个测试函数执行之前被调用
void init();
/// @brief 每个测试函数执行之后被调用
void cleanup();
//测试加法运算
void addTwoNumber_twoPositiveNum_returnInt();
void addTwoNumber_twoNegative_returnInt();
void addTwoNumber_nagativeandpostive_returnInt();
void addTwoNumber_twozero_returnInt();
//toIso8601String 接口测试(缺省值和外部设置值)
void toIso8601String_whenDefaultValue_returnsString();
void toIso8601String_whenValueSet_returnsString();
//toPrettyDateString 接口测试(缺省值和外部设置值)
void toPrettyDateString_whenDefaultValue_returnsString();
void toPrettyDateString_whenValueSet_returnsString();
//toPrettyTimeString 接口测试(缺省值和外部设置值)
void toPrettyTimeString_whenDefaultValue_returnsString();
void toPrettyTimeString_whenValueSet_returnsString();
//toPrettyTimeString 接口测试(缺省值和外部设置值)
void toPrettyString_whenDefaultValue_returnsString();
void toPrettyString_whenValueSet_returnsString();
private:
//测试数据
QDateTime testDate{QDate(2022, 6, 4), QTime(16, 40, 32)};
};
}
#endif // DEMOTEST_H
//demotest.cpp
#include "demotest.h"
#include <QDebug>
#include "demo.h"
namespace test {
//实例化静态变量自动添加到列表中
static DemoTest instance;
DemoTest::DemoTest():TestSuite("demoTest")
{
}
void DemoTest::initTestCase()
{
}
void DemoTest::cleanupTestCase()
{
}
void DemoTest::init()
{
}
void DemoTest::cleanup()
{
}
void DemoTest::addTwoNumber_twoPositiveNum_returnInt()
{
Demo demo;
QCOMPARE(demo.addTwoNumber(1955,1932), 3887);
}
void DemoTest::addTwoNumber_twoNegative_returnInt()
{
Demo demo;
QCOMPARE(demo.addTwoNumber(-1955,-1932), -3887);
}
void DemoTest::addTwoNumber_nagativeandpostive_returnInt()
{
Demo demo;
QCOMPARE(demo.addTwoNumber(-1955,1932), -23);
}
void DemoTest::addTwoNumber_twozero_returnInt()
{
Demo demo;
QSignalSpy valueChangedSpy(&demo, &Demo::inputtwozero);
QCOMPARE(demo.addTwoNumber(0,0), 0);
QCOMPARE(valueChangedSpy.count(), 1);
}
void DemoTest::toIso8601String_whenDefaultValue_returnsString()
{
Demo demo;
QCOMPARE(demo.toIso8601String(), QString(""));
}
void DemoTest::toIso8601String_whenValueSet_returnsString()
{
Demo demo(testDate);
QString string_value = demo.toIso8601String();
QCOMPARE(demo.toIso8601String(), QString("2022-06-04T16:40:32"));
}
void DemoTest::toPrettyDateString_whenDefaultValue_returnsString()
{
Demo demo;
QCOMPARE(demo.toPrettyDateString(), QString("Not set"));
}
void DemoTest::toPrettyDateString_whenValueSet_returnsString()
{
Demo demo(testDate);
QCOMPARE(demo.toPrettyDateString(), QString("4 Jun 2022"));
}
void DemoTest::toPrettyTimeString_whenDefaultValue_returnsString()
{
Demo demo;
QCOMPARE(demo.toPrettyTimeString(), QString("Not set"));
}
void DemoTest::toPrettyTimeString_whenValueSet_returnsString()
{
Demo demo(testDate);
QString pm = demo.toPrettyTimeString();
QCOMPARE(demo.toPrettyTimeString(), QString("04:40 pm"));
}
void DemoTest::toPrettyString_whenDefaultValue_returnsString()
{
Demo demo;
QCOMPARE(demo.toPrettyString(), QString("Not set"));
}
void DemoTest::toPrettyString_whenValueSet_returnsString()
{
Demo demo(testDate);
QCOMPARE(demo.toPrettyString(), QString("周六 4 6月 2022 @ 16:40:32"));
}
}
添加完测试用例之后,执行测试工程,我们就可以在输出目录里面查看测试结果了,通过测试结果,我们就可以知道哪个测试成功,哪个测试失败了。测试文件数据格式如下:
<?xml version="1.0" encoding="UTF-8" ?>
<testsuite errors="0" failures="1" tests="14" name="test::DemoTest">
<properties>
<property value="5.9.0" name="QTestVersion"/>
<property value="5.9.0" name="QtVersion"/>
<property value="Qt 5.9.0 (i386-little_endian-ilp32 shared (dynamic) debug build; by MSVC 2015)" name="QtBuild"/>
</properties>
<testcase result="pass" name="initTestCase"/>
<testcase result="pass" name="addTwoNumber_twoPositiveNum_returnInt"/>
<testcase result="pass" name="addTwoNumber_twoNegative_returnInt"/>
<testcase result="pass" name="addTwoNumber_nagativeandpostive_returnInt"/>
<testcase result="pass" name="addTwoNumber_twozero_returnInt"/>
<testcase result="pass" name="toIso8601String_whenDefaultValue_returnsString"/>
<testcase result="pass" name="toIso8601String_whenValueSet_returnsString"/>
<testcase result="pass" name="toPrettyDateString_whenDefaultValue_returnsString"/>
<testcase result="pass" name="toPrettyDateString_whenValueSet_returnsString"/>
<testcase result="pass" name="toPrettyTimeString_whenDefaultValue_returnsString"/>
<testcase result="pass" name="toPrettyTimeString_whenValueSet_returnsString"/>
<testcase result="pass" name="toPrettyString_whenDefaultValue_returnsString"/>
<testcase result="fail" name="toPrettyString_whenValueSet_returnsString">
<failure message="Compared values are not the same
Actual (demo.toPrettyString()) : "Sat 4 Jun 2022 @ 16:40:32"
Expected (QString("周六 4 6月 2022 @ 16:40:32")): "\uFFFD\uFFFD\uFFFD\uFFFD 4 6\uFFFD\uFFFD 2022 @ 16:40:32"" result="fail"/>
</testcase>
<testcase result="pass" name="cleanupTestCase"/>
<system-err/>
</testsuite>
from: https://blog.csdn.net/yang1fei2/article/details/125121777