基于QtTest的自动化单元测试

2017-05-11 17:29朱健
价值工程 2017年14期
关键词:单元测试自动化

朱健

摘要: Qt是跨平台的应用软件开发框架,Qt Test是Qt框架的单元测试库。基于Qt Test的单元测试可以进行功能和性能测试。持续集成系统可以自动化单元测试的构建、部署、运行和结果统计过程。工程实践表明:基于Qt Test的单元测试与持续集成系统的结合可以降低软件的缺陷率,优化软件架构的设计,提高软件工程的自动化。

Abstract: Qt is a cross platform application development framework. Qt Test is the unit testing library provided by Qt framework. The Qt Test based unit testing can be used for functionality and benchmarking testing. Continuous integration system can automate the build, deployment, run and result statistic processes of unit testing. The engineering practice shows the combination of Qt Test based unit testing and continuous integration system can be used for decreasing the defect rate of software, optimizing the design of software architecture, improving the automation of software engineering.

關键词: 单元测试;Qt;持续集成;自动化

Key words: unit testing;Qt;continuous integration;automation

中图分类号:TP31 文献标识码:A 文章编号:1006-4311(2017)14-0216-04

0 引言

基于Qt框架的大型应用软件通常包括用户界面,业务逻辑,工具库,UI组件库,平台适配层等,软件规模大,逻辑复杂,通常进行层次化和模块化设计,软件的整体稳定性和效率与每个基础模块的代码质量息息相关。单元测试是对软件模块的功能测试,可及早发现软件设计和实现中的问题,便于问题的定位[2]。为了降低软件的缺陷率,业界提出并实施了测试驱动开发(Test Driven Development)方法,该方法主张先开发单元测试程序,由单元测试结果驱动软件的设计和编码,以到达更具针对性的架构设计和更低的首次发布缺陷率[6,7]。所以单元测试是大型尤其是采用测试驱动开发的Qt软件工程的必然要求。

在工程实践中常用的通用单元测试框架包括:适用于C++语言的CppUnit[3],适用于Java语言的JUnit[2]等。但是基于Qt的软件大都依赖于Qt对C++语言的扩展,如信号-槽(signal-slot)机制,属性(property)机制,元对象(meta-object)框架等;构建过程依赖于Qt特有的工具链(如qmake,moc等);运行过程依赖于Qt特有的基础类库,所以很难使用CppUnit等通用单元测试框架对基于Qt的软件进行单元测试。而Qt Test是Qt框架的单元测试模块,可以与Qt的工具链、基础类库、应用框架、集成开发环境(IDE)等良好结合[5]。

1 Qt Test单元测试框架分析

1.1 Qt Test单元测试原理

在Qt Test单元测试体系中,每个测试文件是一个测试集合,测试集合是一个继承自QObject的测试对象(Test Object),测试对象的每个私有槽(private slot)都是一个测试函数。Qt Test单元测试框架通过QObject::metaObject()获取测试对象的QMetaObject属性,通过QMetaObject::method()依次获取测试对象的私有槽,逐个调用并输出测试结果。一个典型的测试对象可以用如下的类定义:

class TestSuite : public QObject {

Q_OBJECT

public:

TestSuite();

private Q_SLOTS:

void initTestCase();

void cleanupTestCase();

void testcase1();

void testcase2();

};

其中initTestCase()和cleanupTestCase()是两个特殊的测试函数,initTestCase()在测试程序开始运行时调用,主要用于准备测试环境,包括构造测试相关的对象、桩对象、其他基础Qt库对象,初始化局部变量,进行数据库连接,准备测试数据等。cleanupTestCase()在测试程序结束运行前调用,是initTestCase()的逆过程,用于销毁测试过程中构造的对象资源,删除测试运行过程中生成的数据等。其余的私有槽都是独立测试函数,测试函数的实现需要根据测试例流程进行一系列的操作,验证功能和性能要求是否达到预期。

每个Qt Test测试文件最终构建成一个单元测试程序,单元测试程序可以独立运行,不需要运行工具(Test Runner),可以在资源受限的嵌入式Linux设备上运行。单元测试的运行流程如图1:测试程序启动后,测试框架通过QTest::qExec()运行测试对象。首先,测试对象的initTestCase()被调用;之后测试对象的各个测试函数会被依次调用,并记录测试结果;最后cleanupTestCase()被调用,测试结果输出,单元测试结束。

1.2 Qt Test单元测试内容

1.2.1 功能性测试

单元测试的基本功能是进行功能性的测试,通过调用待测对象的接口,完成测试例规定的流程,比对期望值和实际值,并输出测试结果。Qt Test提供一系列宏,用于在测试代码中比对期望值与实际值。例如,QVERIFY(expr)用于校验二元表达式expr,若expr为假(false)则判定当前测试例失败;QCOMPARE(value1, value2)用于判断两个值value1,value2是否相等,若不相等则判定测试例失败。

在功能性覆盖率达到100%的情况下,需要进一步提高代码路径覆盖率。为提高单个测试例函数的代码路径覆盖率,可以采用数据驱动测试(Data Driven Test)方法:即设计多组数据,多次运行该测试例函数,以提高测试效率。可根据待测模块的代碼实现、测试例的需求规约,设计出覆盖边界条件、错误处理分支的测试数据集,提高单元测试的代码路径覆盖率。Qt Test支持数据驱动测试:实现以"_data"为结尾的测试函数为同名测试函数提供测试数据,测试代码通过QFETCH获取测试数据。以下代码构造了{20, 23, "Type A"}, {2000, 2098, "Type B"}两组数据对代码中的算法Classifier::doClassifier()进行了测试。

void TestSuite::testcase1_data() {

QTest::addColumn("inputX");

QTest::addColumn("inputY");

QTest::addColumn("result");

// 第一组测试数据

QTest::newRow << 20 << 23 << QString("Type A");

// 第二组测试数据

QTest::newRow << 2000 << 2098 << QString("Type B");

}

void TestSuite::testcase1() {

// 获取数据并测试

QFETCH(int, inputX);

QFETCH(int, inputY)

QFETCH(QString, result);

QString type = Classifier::doClassify(inputX, inputY);

QVERIFY(type == result);

}

1.2.2 GUI测试

通常的GUI测试主要是黑盒测试,通过模拟键盘、鼠标事件驱动GUI应用程序运行,捕捉屏幕上的GUI对象比对测试结果,需要复杂的Test Runner支持[1],而且这类GUI测试不能对单个GUI控件进行单元测试。Qt Test为继承自QWidget的控件提供了GUI单元测试功能:使用QTest::keyClick(),QTest::mouseClick()模拟键盘和鼠标事件驱动控件运行;通过QVERIFY,QCOMPARE比对期望值和实际结果,不需要复杂的对象捕捉技术。GUI测试同样支持数据驱动的测试,QTestEventList用于定义作用在QWidget上的事件序列,例如:

QTestEventList events;

CustomWidget w;

// 定义事件序列

events.addMouseClick(Qt::LeftButton, 0, QPoint(20, 30));

events.addKeyClick(Qt::Key_Backspace);

events.addDelay(100);

// 模拟运行事件序列

events.simulate(&w);

// 比对测试结果

QCOMPARE(w.myProperty(), expectedValue);

Qt Test的GUI测试不依赖任何测试工具,以轻量级方式模拟鼠标、键盘事件,便于团队在设计和实现GUI组件时做充分的测试,避免系统集成时因GUI组件缺陷导致系统异常。

1.2.3 性能测试

软件的整体性能与基础模块的性能密切相关,只对基础模块做功能性单元测试,不能保证其性能指标。Qt Test可以使用QBENCHMARK进行性能测试。QBENCHMARK可以对块内代码片段的运行时间进行测定。单元测试运行时,QBENCHMARK块内的代码片段会被运行多次并记录运行时间,从而得到比较精确的结果。性能测试的反复运行次数,性能计量单位,最小可接受的性能值等可以通过单元测试命令行参数设定。在单元测试中引入性能测试,可以在设计和实现阶段及时发现性能问题,避免软件集成时运行效率无法达标的问题;在软件维护阶段,基础模块的性能测试也可以及时定位不当修改引入的性能问题,保证软件质量平稳。

1.3 Qt Test单元测试的改进

单元测试过程应该由大到小,划分为若干测试模块,最终细化到函数级别,在函数级别完成逻辑性测试,覆盖代码分支,再由小到大完成集成测试和功能性测试[2]。从代码分支到系统功能的充分测试,可以及时发现系统中存在的问题,提高测试效率,降低开发后期的测试和维护成本。软件的设计和实现需要遵循如下原则:①将复杂的大模块拆分成适合于单元测试的小模块。对每个小模块进行单独的单元测试,有利于缺陷的定位和排除。②将业务逻辑与框架做分离。软件设计时应该避免将业务逻辑与框架耦合在一起,否则会造成无法通过构造模拟数据进行单元测试。③基础模块的单个函数不能实现过多功能,复杂的函数会造成测试粒度过大,不利于缺陷定位,复杂函数应拆分为较小的函数。④消除冗余代码。冗余代码会增加单元测试的设计难度,降低测试效率,代码实现时需要消除冗余变量、冗余接口,提取冗余代码的公共部分为独立的函数。

2 自动化Qt Test单元测试方法

2.1 自动化Qt Test单元测试的流程和工具

大型Qt应用程序通常包含大量子模块,本身需要开发大量的单元测试程序进行测试;在测试驱动的开发中,需要先设计和实现单元测试用例,再进行功能模块的开发,随着开发的迭代,单元测试工程数量必然逐步增多。所以,在实际项目中,单元测试工程的开发和维护工作量是很大的。为了提高单元测试的开发和维护效率,可以使用Qt Creator集成开发环境(IDE)。Qt Creator可以将多个的单元测试工程以子工程(sub project)的形式进行管理,有利于单元测试工程的分类和维护;新建单元测试工程时,Qt Creator可以为开发者生成框架代码,避免重复劳动。

随着单元测试程序数量增加,由开发者手动运行每个单元测试程序并查看结果将是繁重的重复性劳动,且不能对单元测试中的警告、错误、失败率等进行有效的统计和归档,项目管理者也无法及时了解单元测试的进度、状况等。持续集成(Continuous Integration)系统可以自动化单元测试的构建、部署、运行、结果统计过程。持续集成是一种软件工程实践,即通过工具自动从源代码管理系统获取项目的源代码,自动编译,部署,运行软件。持续集成能够及时发现和解决软件工程中存在的问题,减轻开发者的日常工作量,为项目管理者提供可视化的数据参考,Jenkins[4]是目前业内广泛使用的持续集成系统。Qt Creator和Jenkins系统相结合可以有效提高Qt Test单元测试各步骤的自动化程度(如图2),提高Qt Test单元测试的效率,实现基于Qt Test的测试驱动开发。

2.2 使用Qt Creator开发和维护Qt Test单元测试工程

Qt Creator是Qt默认的集成开发环境(IDE),使用Qt Creator工程向导可以快速创建Qt Test单元测试工程。可以通过“文件->新建文件或项目->其他项目->Qt单元测试”打开“Qt单元测试”向导(图3)。

在Qt单元测试向导分为5个步骤:位置,构建套件(Kit),模块,详情,汇总,“位置”用于设置单元测试工程路径,“构建套件”用于指定目标代码的编译器或交叉编译器,“模块”用于添加单元测试工程依赖的Qt基础类库,详情用于生成单元测试框架代码。向导生成单元测试工程的框架代码,开发者只需进一步增加和完善测试代码。开发者可以在Qt Creator中编译和运行單元测试代码,最终完成单元测试工程的开发。

2.3 使用Jenkins自动化构建、部署、结果统计

在Jenkins系统Web界面的“项目->配置->构建”页面可编写自动化构建、部署、运行单元测试程序的脚本,在Linux环境下采用Shell脚本编写。“项目->配置->构建后操作”页面可以添加“Publish xUnit test report”步骤用于呈现Qt单元测试程序生成的基于XML的测试结果。

以自动化嵌入式Linux设备的单元测试为例,首先需要在Jenkins编译主机上交叉编译生成可执行的Qt单元测试程序,对于支持轻量级SSH服务如dropbear的Linux设备,可以采用SSH(Secure Shell)协议进行单元测试程序的部署、运行及结果回收。以下是自动化脚本的主要代码:

# 编译单元测试程序

mkdir $WORKSPACE/build

mkdir $WORKSPACE/targetfs

cd build

qmake ..

make INSTALL_ROOT="$WORKSPACE/targetfs" install

cd $WORKSPACE/targetfs/

# 将单元测试程序部署到远程设备

scp tst_* root@://

# 在远程设备上运行单元测试程序

ssh root@ //remote_run.sh

# 将单元测试结果收回本地

scp root@//report_*.xml $WORKSPACE/targetfs//

此例中,单元测试程序可执行文件名以tst_开头,remote_run.sh位于待测Linux设备上,用于批量运行单元测试程序并输出XML格式的测试结果,remote_run.sh的核心代码如下:

# 枚举单元测试程序

for unittest in tst_*

do

# 执行单元测试程序,并导出XML格式的测试结果

bash -c "./${unittest} -xml -o report_{unittest}.xml"

done

Jenkins的“xUnit test report”插件可以根据XML格式的Qt Test测试结果以测试结果趋势图(图4)、详细测试结果表格(图5)的形式呈现出来。测试结果趋势图页面可以形象地展现近期单元测试总数、通过率、失败率的变化趋势;详细测试结果表格页面展示所有测试例的结果详情,包括失败测试例的失败信息、各测试例的运行结果和执行时间等。

3 结论

在测试驱动的开发中,需要先完成单元测试代码,根据单元测试结果迭代式地设计、实现、改进目标软件,最终交付软件产品。这种以单元测试为基础的软件开发模型,可以对软件基础模块的基本功能、关键性能等做充分的测试,可以尽早发现、定位、解决软件缺陷,改善软件质量,优化软件架构。

随着软件规模的扩大,单元测试的规模会逐步增大,构建、运行、检查单元测试结果的工作量也随之上升,通过持续集成系统进行自动化单元测试的实践可以减轻开发人员的构建、部署、测试工作量,提高测试驱动开发的自动化程度,在持续集成系统中实时展示单元测试结果可以提高开发者定位、修正软件缺陷的效率,可以为软件工程的进度和质量提供定量化的数据参考。

参考文献:

[1]吴立金,唐龙利,韩新宇,等.嵌入式软件GUI自动化测试平台研究[J].计算机测量与控制,2015,23(4):1094-1097.

[2]张敏,陈静,王娟.基于MVC模式的Web系统自动化单元测试方案[J].微型电脑应用,2016,32(2):78-80.

[3]贾长伟,廖建,焉宁,等.基于CppUnit的虚拟试验单元测试研究[J].计算机测量与控制,2015,23(4):1155-1160.

[4]John Ferguson Smart. Jenkins The Definitive Guide[M]. O'Reilly Media, 2011: 169-180.

[5]Qt Test user manual. http://doc.qt.io/qt-4.8/qtestlib-manual.html 2016.

[6]Nachiappan Nagappan, E. Michael Maximilien, Thirumalesh Bhat, Laurie Williams. Realizing quality improvement through test driven development: results and experiences of four industrial teams[J]. Empir Software Eng 2008,13: 289-302.

[7]Kent Beck. Test-Driven Development By Example[M]. Addison Wesley, 2002.

猜你喜欢
单元测试自动化
《一次函数》单元测试题
《一次函数》单元测试题