本文档在对CPPUINT简单介绍的基础上,主要讲解针对Android的C/C++代码,如何利用CPPUINT进行单元测试的方法和步骤。
1、 CPPUINT简介
CPPUNIT是基于JUnit衍生而来的,专门面向C++代码的测试框架。
其主要功能有:
n 带有附加数据的XML输出
n 集成到某个IDE中,以编译文本方式进行结果输出
n 更便于测试套件声明的辅助宏定义
n 支持分等级的测试环境
n 特有的测试注册方式,降低了程序重新编译的需要
n 能够加快编译、测试周期的测试插件
n 封装了测试执行过程的保护器
2、 测试方法
<<可执行程序>> 测试程序 |
<<库>> 待测试软件模块 |
<<库>> CPPUNIT测试框架 |
图1 基本测试方法调用关系图
基于CPPUNIT测试框架的测试程序,其基本的测试方法如图1所示:CPPUNIT测试框架以一个动态库的形式存在,而需要进行单元测试的软件模块(称为待测试模块)则是以动态库或静态库的形式存在。为了进行单元测试,需要专门开发一个或多个测试程序,测试程序包含测试用例的代码,并同时链接待测试模块和CPPUNIT测试框架。通过执行测试程序完成单元测试,基本步骤如下:
(1) 编译CPPUINT测试框架,生成动态库;
(2) 准备好待测试软件模块;
(3) 编写测试程序并编译;
(4) 执行测试程序。
3、 测试步骤
3.1编译CPPUNIT测试框架生成动态库
CPPUNIT测试框架是一个开源项目,利用Android的编译系统将CPPUNIT编译为Android平台下的动态链接库,以方便开发人员在后期开发测试程序时,提供调用接口。具体的编译步骤如下:
(1) 将CPPUNIT的源代码复制到Android的源码树中的某个位置;
(2) 在CPPUNIT的源代码目录的根目录下编写相应的Android.mk工程文件,其编写方法可以参考附录A中的内容;
(3) 在Android源代码的根目录下执行如下指令,进行编译:
# make <module-name> ONE_SHOT_MAKEFILE=<path to Android.mk of cppunit> TARGET_PRODUCT=<product-name>
其中:
l module-name:所需要编译的项目模块名称,这个名称是在项目的Android.mk文件中定义的
l path to Android of cppunit:CPPUNIT项目的Android.mk文件的路径,是相对于Android源码根目录的相对路径
l product-name:目标产品名称
示例如下:
make cppunit ONE_SHOT_MODULE=./cppunit/Android.mk PRODUCT=eeepc
通过上述步骤编译过后,就可以在Android源代码目录out/target/product/eeepc下找到编译好的动态库 libcppunit.so了。
3.2准备待测试模块
根据软件设计规范,实现待测模块的代码开发,编译生成动态库或静态库,供测试程序调用。
3.3编写测试程序
3.3.1 测试程序基本结构
基于CPPUNIT测试框架的测试程序由三个部分组成:
n 测试运行环境:测试程序的运行控制主体,一个测试程序具有唯一的测试运行环境,该测试运行环境中会包含多个测试套件;
n 测试套件(Testsuite):一组具有一定相关性的测试用例的集合;
n 测试用例(Testcase):用于完成一个独立的测试功能。
三者之间的关系如图2所示:
图2 测试程序基本组织图
3.3.2测试程序的编写步骤
根据图2的组织结构,测试程序的开发一般采用如下步骤:
(1) 编写测试程序主函数,利用CPPUNIT测试框架所提供的类为测试程序创建测试运行环境、执行测试过程,并输出测试结果;
(2) 设计需要的测试用例,并将测试用例组织为若干个测试套件;
(3) 为每一个测试套件设计为一个类,该类继承自CPPUNIT测试框架的TestFixture类,测试用例实现为测试套件类的成员函数。CPPUNIT提供了一些宏定义,使定义测试套件类和测试用例变得很简单,从而使得测试程序的开发人员可以专注于测试用例的具体实现;
在测试程序中需调用的CPPUNIT测试框架核心类以及各个类之间的关系如图3所示。其中灰色部分的“测试套件类”是由测试程序所编写:
图3 测试程序中主要类关系图
3.3.3头文件和名字空间
由图3所示,在程序中需要包含如下头文件:
在main.cpp文件中包含如下:
<cppunit/BriefTestProgressListener.h>
<cppunit/CompilerOutputter.h>
<cppunit/extensions/TestFactoryRegistry.h>
<cppunit/TestResult.h>
<cppunit/TestResultCollector.h>
<cppunit/TestRunner.h>
在测试套件类声明头文件中包含如下:
<cppunit/extensions/HelperMacros.h>
另外CPPUNIT定义了一个专有名字空间CPPUNIT_NS,框架中的各个类都是在该名字空间中声明的,在使用CPPUNIT的类时,需使用该名字空间。
3.3.4主函数编写方法
主函数是整个测试程序的入口,其主要功能是完成测试环境的创建、接受测试套件的注册申请、执行测试过程并将测试结果输出。
下面是主函数的示例代码。
//---- main.cpp ------ #include <cppunit/BriefTestProgressListener.h> #include <cppunit/CompilerOutputter.h> #include <cppunit/extensions/TestFactoryRegistry.h> #include <cppunit/TestResult.h> #include <cppunit/TestResultCollector.h> #include <cppunit/TestRunner.h>
int main( int argc, char* argv[] ) { // Create the event manager and test controller CPPUNIT_NS::TestResult controller;
// Add a listener that colllects test result CPPUNIT_NS::TestResultCollector result; controller.addListener( &result );
// Add a listener that print dots as test run. CPPUNIT_NS::BriefTestProgressListener progress; controller.addListener( &progress );
// Add the top suite to the test runner CPPUNIT_NS::TestRunner runner; runner.addTest( CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest() ); runner.run( controller );
// Print test in a compiler compatible format. CPPUNIT_NS::CompilerOutputter outputter( &result, CPPUNIT_NS::stdCOut() ); outputter.write();
return result.wasSuccessful() ? 0 : 1; }
|
通常情况下,测试程序只需要原封不动地使用上述代码即可。下面简要介绍该函数各部分的含义,不再做进一步的详细说明。
(1) 创建一个事件管理和测试控制器
CPPUNIT_NS::TestResult controller;
(2) 创建一个监听用于收集测试结果
CPPUNIT_NS::TestResultCollector result;
controller.addListener( &result );
(3) 将顶层测试套件注册到测试执行器并执行
CPPUNIT_NS::TestRunner runner;
runner.addTest( CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest() );
runner.run( controller, testPath );
(4) 显示测试结果
CPPUNIT_NS::CompilerOutputter outputter( &result,
CPPUNIT_NS::stdCOut() );
outputter.write();
3.3.5测试用例编写方法
测试用例实现为测试套件类的成员函数,用于完成对“待测试模块”的测试工作。根据测试场景,在测试用例中设计调用“待测试模块”的各项功能,诊断这些功能是否符合预期效果。
这一部分是测试程序中最核心的部分。测试程序一般采用白盒测试的方式进行。在编写测试用例时,采用不同的分支测试函数调用出现的各种结果。在不同的分支流程下,诊断函数接口的功能实现是否符合预期的效果,从而检测程序的稳定性。
CPPUNIT为测试程序提供了诊断宏定义,以方便开发人员使用这些宏定义诊断函数接口的调用情况。
这些宏定义及其作用如下:
n CPPUNIT_ASSERT(condition): 诊断condition为真
n CPPUNIT_ASSERT_MESSAGE(message, condition): 当condition为假时失败, 并打印message
n CPPUNIT_FAIL(message):当前测试失败, 并打印message
n CPPUNIT_ASSERT_EQUAL(expected, actual):诊断两者相等
n CPPUNIT_ASSERT_EQUAL_MESSAGE(message, expected, actual):失败的同时打印message
n CPPUNIT_ASSERT_DOUBLES_EQUAL(expected, actual, delta):当expected和actual之间差大于delta时失败
测试用例的具体实现由测试程序开发人员自由发挥,在此不再赘述。
3.3.6测试套件编写方法
测试套件的作用就是将测试用例组织和集中起来。在测试程序中,一个测试套件就是一个类,该类继承自CPPUNIT框架的TextFixture类,并且它所包含的测试用例都是以该类的成员函数形式存在的。一个测试程序可以包含一个或者多个测试套件类。一个测试套件类主要有三部分组成:
(1) 类定义
下面是一个测试套件类的示例代码:
//----ExampleTestsuite.h------ #include <cppunit/extensions/HelperMacros.h>
class ExampleTestsuite : public CPPUNIT_NS::TestFixture { CPPUNIT_TEST_SUITE( ExampleTestsuite ); CPPUNIT_TEST( testAdd ); CPPUNIT_TEST( testEquals ); CPPUNIT_TEST_SUITE_END();
protected:
void testAdd(); void testEquals(); }; |
首先是常规定义。CPPUNIT提供了下列三个宏定义来帮助测试套件类完成常规定义:
l CPPUNIT_TEST_SUITE() 开始测试套件的常规定义,其参数必须与类名相同;
l CPPUNIT_TEST() 添加一个测试用例;
l CPPUNIT_TEST_SUITE_END() 结束测试套件的常规定义
这部分必须以CPPUNIT_TEST_SUITE()开始,以CPPUNIT_TEST_SUITE_ END()结束,在这两个宏之间可加入若干个CPPUNIT_TEST()宏,每一个CPPUNIT_TEST()宏注册一个测试用例。在上述例子中,测试套件ExampleTestsuite中包括两个测试用例testAdd和testEquals。
在常规定义之后是测试用例成员函数的定义。每一个测试用例对应于一个成员函数,函数名需与CPPUNIT_TEST()的参数一一对应。
除了常规定义和测试用例函数定义之外,为了实现测试用例的功能,开发人员还可以在测试套件类中任意定义其它成员变量和成员函数,符合C++的语法规范要求即可。
(2) 类实现
测试套件类的实现包括对每一个测试用例函数的实现以及开发人员自定义的其它成员函数的实现。除此之外,在CPPUNIT_TEST_SUITE()宏定义中还重载了两个来自父类TestFixture的虚函数,测试套件类必须提供这两个函数的实现:
Virtual void setup(); // 一般用于测试环境的初始化
virtual void teardown(); // 用于测试环境的清理
CPPUNIT框架在执行一个测试套件之前,首先会调用setup()成员函数;在测试套件执行结束后,会调用teardown()成员函数。开发人员根据需要实现这两个函数。
下面是一个测试套件类实现的示例代码。其中在实现的开始处,必须调用一个宏CPPUNIT_TEST_ SUITE_REGISTRATION(),这个宏的功能在后面会讲到。
//------ExmpleTestcase.cpp---- #include <cppunit/config/SourcePrefix.h> #include "ExampleTestsuite.h" #include <mut.h> // 待测模块的头文件
CPPUNIT_TEST_SUITE_REGISTRATION(ExampleTestsuite);
void ExampleTestsuite::setup() { }
void ExampleTestCase::teardown() { }
void ExampleTestCase::testAdd() { Double v1, v2; v1 = 2.0; v2 = 4.0; double result = mut_add(v1, v2); CPPUNIT_ASSERT( result == 6.0 ); }
void ExampleTestCase::testEquals() { long* l1 = new long(12); long* l2 = new long(12);
CPPUNIT_ASSERT ( mut_equals(l1, l2) ); *l1 = 10; CPPUNIT_ASSERT ( !mut_equals(l1, l2) ) } |
(3) 对象实例声明
测试程序需为测试套件类声明一个对象实例,并将该对象实例添加到测试工厂注册器。测试工厂注册器是一个TestFactoryRegister的对象实例,负责维护各个测试套件类的实例。测试运行环境是通过测试工厂注册器来获取测试套件对象实例的,因此只有注册过的测试套件才会被测试运行环境所执行。CPPUNIT提供了下列宏来完成这一注册工作:
l CPPUNIT_TEST_SUITE_NAMED_REGISTRATION() 将一个测试套件类的实例添加到指定的测试工厂注册器。
l CPPUNIT_TEST_SUITE_REGISTRATION() 将测试套件类实例添加到缺省测试工厂注册器
3.4执行测试程序
3.4.1测试消息输出方法
CPPUNIT测试框架允许测试用例在执行过程中输出信息,并提供了三种输出方式:控制台、MFC、QT等。对于Android平台,目前仅支持输出到控制台(Console)。采用这种方式输出,只需要在编写测试程序时,在主函数中调用CPPUNIT_NS名字空间下的TestResult类创建对象即可:
// Create the event manager and test controller
CPPUNIT_NS::TestResult controller;
3.4.2测试消息分析
测试程序的可执行文件可以在控制台shell下直接运行,在运行过程中,会在控制台上输出各种信息,测试人员通过这些信息了解测试的进展情况和测试结果。下面是一个输出信息的示例。
图4 测试消息输出示例图
测试消息主要有三部分组成:
(1) 参加测试的测试套件以及测试用例的名称
测试消息首先显示参加测试的测试套件以及包含的测试用例的名称,这些名称是通过在之前进行测试套件类定义时,使用CPPUNIT框架的宏定义获取到的。
结合图4,如下图示例所示:
该测试程序只有一个测试套件ExampleTestCase,其中包含四个测试用例:Example、anotherExample、testAdd、testEquals,并且每个测试用例属于断言类测试。
(2) 断言出现异常的位置
在该部分主要详细显示测试用例出错的信息,其中包括:断言出错点在测试程序中的详细位置、出错类型、测试用例名称、断言信息以及诊断方法等。
以所给出的示例进行详细说明:
从断言信息可以判断,出错的位置是在ExampleTestCase.cpp文件的第8行,属于断言类错误,所测试的是测试套件ExampleTestCase下的example测试用例,诊断出错的原因是测试的浮点数相等性断言失败,具体是由于:期望值是1,而实际值是1.1,超过此前断言规定的误差0.05的范围。这些信息的显示和处理都是在编写具体的测试用例时,由事先设计和调用断言宏实现的。根据上述断言消息,我们能够很快找到测试程序中相应位置的具体调用情况。如下所示:
Result = Test( );
CPPUNIT_ASSERT_DOUBLES_EQUAL( 1.0, Result, 0.05 );
通过测试消息的详细说明,开发人员很快定位到断言宏的调用位置并且了解到接口的执行情况,发现代码的Bug并及时进行修改。
(3) 测试结束时的统计信息
在测试消息的最后,测试程序会给出一个最终的测试结果,并且统计整个测试程序的执行情况:执行测试用例的总数、失败测试用例的个数、测试失败的测试用例的个数、执行失败的测试用例的个数等信息。
如示例所给出的信息如下所示:
由于测试过程中出现了测试用例失败的情况,因此给出的测试结果是失败的。总共执行的测试用例是4个,失败的测试用例数为4个,其中属于测试失败的测试用例为4个,在执行过程没有出现执行出错的测试用例。
附录A CPPUNIT框架的工程(供参考)
在Android源代码包中添加CPPUNIT项目,将CPPUNIT所有源代码添加到Android目录下
在CPPUNIT源代码目录下编写Android.mk文件,根据Android.mk的语法规范编写,具体的Android.mk文件的编写规范可参考NDK文件中docs/目录下的ANDROID-MK.TXT
示例如下:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := libcppunit LOCAL_CPP_EXTENSION := src/.cxx LOCAL_PRELINK_MODULE := false Include $(BUILD_SHARED_LIBRARY) |