【IT168技术】概述
本文档是一篇实用指南,它介绍了单元测试、设计单元测试的技术以及在S60平台上创建和运行单元测试时可使用的工具。我们所期望的读者是对他们自有的代码模块进行编写和运行的开发者。一般来说单元测试是一个庞大的体系,它包括无穷多的技巧、定义和方法。通常的办法是将测试分为不同的级层,比如:
* 单元测试,指软件的每个单元(通常是单个的类)都用来被(通常是开发者)测试以确认单元的设计细节运行无误。
* 整体测试,指由各组件(小规模且测试过的组件)合成的整体软件被拿来测试。
* 系统测试,指由各组件最终整合而成的软件被拿来测试以显示所要的需求。
* 可接受性测试,指通常由客户运行以测试决定所提交的软件是否可被接受。
一个小的程序可由一个小的单元生成,没有独立的整体测试级层。对于大的系统,把由小单元合成的大组件分割看来是很明智的。在这种情况下,整体测试在确保整体性不会扰乱各个基本功能函数的过程中起到了非常重要的作用。
单元测试大多数是被开发者设计和运行的,这就意味着在开发初期就能检测到错误,所以纠正它们要比在系统测试或是接受性测试时小很多开销。单元测试、测试驱促开发和测试框架被Richard Carlsson和Micka??l Rémond[13]很好地联系起来;请看附录C。
在S60 C++开发平台上的一个很好的方式是将算法和业务逻辑与用户接口分开。通常我们会有无UI依赖性的驱动DLL,于是单元测试仅被用于组成DLL的类。对驱动DLL的整体测试通常被称为“组件测试”,它在整体测试级层终止。组成DLL的不同类要被分别测试,并在单元测试级层终止。
本文档的其他部分涉及了单元测试技术和S60 C++开发平台的观点的实践。如上所述,我们期望的读者是开发者,但同时其他行业的读者也能从中得到有趣的内容和信息。
Symbian C++ 单元测试的应用介绍
在其简易的格式里,单元测试可被执行为无UI的,在查看返回值时即可知道测试是否成功。该执行过程可被应用到运行一个或多个测试。从长远来看,当同时有很多测试程序时,简单的办法是创建一个框架,它能自动地为测试制造环境,然后执行那些小的测试模块并做结果报告,这种框架被称为测试框架。 EUnit[4] 是一个商业软件,而SymbianOSUnit[2],[3]则是免费的。
如果只是测试性地使用SymbianOSUnit,它是免费的。对于大规模开发,制造商业软件,那就需要支付一定的费用,因为它们提供运行时所需要时间和金钱的特性(查看附录B)。
S60平台:地图和定位范例就在测试部分做了拓展,不过仅做演示使用。在真实生活中,单元测试应该在具体的类被应用前即工程开发期间就被编写。单元测试由类 CMapExampleSmsEngine 创建。
● 创建一个测试工程
开发者应该在他们的电脑上安装Carbide C++ 1.2和S60 3rd FP1,还应激活Carbide的命令行工具(从Carbide的开始菜单项里选择Configure environment for WINSCW command)。
从诺基亚论坛下载S60平台:地图和定位范例[1]并解压缩,确保其存放路径为C:\temp\MapEx。 然后从Sourceforge[3]下载SymbianOSUnit并解压缩。将SymbianOSUnit由解压缩后的根目录复制到C:\temp\SymbianOsUnit。SymbianOSUnit需要nmake,所以需下载它[6],解压缩后复制NMAKE.EXE和NMAKE.ERR到路径地址到C:\Program Files\Nokia\Carbide.c++ v1.2\x86Build\Symbian_Tools\Command_Line_Tools.
在此之前,建议读者阅读教程文档和测试框架提供的例子。
现在我们开始创建单元测试工程和一些测试:
1.在范例工程下创建路径 C:\vidyasvn\MapEx\test。
2.从测试框架应用教程复制\Tutorial\group\ExtraTestBuildTasks.bldmake和\Tutorial\test\testgen.bat到测试路径。
3.在TestHeader.h文件中创建一个最小的测试套件:任何测试首选的方法就是将它们看作一个测试实例;测试目标作为类变量添加进去并且该测试类(也称为不变量)是继承自CxxTest::TestSuite。
2 #define TESTHEADER_H
3
4 #include "TestSuite.h"
5
6 // forward declaration
7 class CMapExampleSmsEngine;
8 class MSmsEngineObserver;
9
10 class CMapExampleSmsEngineTest : public CxxTest::TestSuite
11 {
12 public:
13 CMapExampleSmsEngineTest(const TDesC8& aSuiteName) :
14 CxxTest::TestSuite(aSuiteName){}
15
16 private: // from CxxTest::TestSuite
17 virtual void setUp();
18 virtual void tearDown();
19
20 public:
21 void testParseMsgCoordinates();
22 void testParseMsgRequestType();
23 void testParseMsgUid();
24 void testSendMessage();
25 void testSendMessageExceptions();
26
27 private: // data
28 MSmsEngineObserver* iObserver;
29 CMapExampleSmsEngine* iTarget;
30 };
31
32 #endif // TESTHEADER_H
33
4.在TestSource.cpp文件中创建空执行函数。
2 #include "Logger.h"
3
4 void CMapExampleSmsEngineTest::setUp(){}
5 void CMapExampleSmsEngineTest::tearDown(){}
6 void CMapExampleSmsEngineTest::testParseMsgCoordinates(){}
7 void CMapExampleSmsEngineTest::testParseMsgRequestType(){}
8 void CMapExampleSmsEngineTest::testParseMsgUid(){}
9 void CMapExampleSmsEngineTest::testSendMessage(){}
10 void CMapExampleSmsEngineTest::testSendMessageExceptions(){}
注:TestDriver.h是在编译过程中由TestHeader.h生成的。
5.为测试创建最小的Symbian makefile指令: SymbianOSUnit.mmp:
2 USERINCLUDE .
3 SOURCEPATH .
4 SOURCE TestSource.cpp
5
6 // test target class definitions & implementations
7 USERINCLUDE ..\inc
8 SOURCEPATH ..\src
9 // SOURCE CMapExampleSMSEngine.cpp // Our tests don’t test actual class yet
10
11 // libraries the test target depends on
12 LIBRARY etext.lib
13
14 // include SymbianOSUnit mmp file from proper
15 // directory depending on relative path and target platform
16 #include "..\..\SymbianOSUnit\SymbianOSUnitApp\group\s60_3rd\SymbianOSUnit.source"
注:CMapExampleSMSEngine源代码被注解停用是因为我们的测试工作还未开始,并且那些组件与其它类相互联系,这使我们需要在目标被测试的时候着意解决的。
6.为测试工程创建一个bld.inf文件:
2 makefile ExtraTestBuildTasks.bldmake
3 SymbianOSUnit.mmp
以上步骤完成以后,要测试的范例路径如图1所示。

图1:工程目录结构图
现在创建测试工程并将其运行在模拟器上。首先,迅速打开命令行将其路径转到 C:\temp\MapEx\test. 然后用bldmake bldfiles命令创建编译文件。其次,用abld makefile指令创建makefiles文件。这是非常重要的过程因为它生成ExtraTestBuildTasks.bldmake指令将生成测试工程的框架。最后,用命令行abld build winscw udeb在模拟器上编译测试工程。这个用来运行测试工程的SymbianOSUnit应用,会出现在菜单里。选择菜单里的“运行所有套件”选项并找到如何执行测试(如图2)。

图2:使用SymbianOSUnit进行单元测试
在上述例子中,所有的测试都是无误运行的。这是我们所期望的,因为这个测试实例是空函数实现的。
● 排除依赖关系
开发者现在应该开始测试实驱动类。首先,包括一个对源文件(TestSource.cpp)测试目标的定义(CMapExampleSmsEngine) #include "cmapexamplesmsengine.h"
然后从.mmp文件中用非注释行的方式把测试目标函数加到工程中。
SOURCE CMapExampleSMSEngine.cpp
这里有一个非常难的部分:在执行的过程中测试性是不被考虑的,所以可能会有联缠,私有区域等困难,使得单元测试具有挑战性。在我们的实例中,会使用信息类>RSendAsMessage和RSendAs,但是模拟结果是很困难的。
解决问题的办法是把默认的库函数替换为开发者自己的。当不能和现有的库相连,但使用开发者所需要的库函数就可实现时,该方法可被使用。当我们缺失一个.mmp文件中的库函数时,编译器将会编译源代码,但链接部分将无法生成最终代码,报错如下:
Undefined symbol: 'void RSendAsMessage::CreateL(class RSendAs &, class TUid) (?CreateL@RSendAsMessage@@QAEXAAVRSendAs@@VTUid@@@Z)'
开发者的任务是使用一些函数实现这种方法,以达到测试需求。此时首先可选择的是简单的空函数,方法是将函数返回值设为NULL或者其他硬编码默认值。注意此方法金用于目标测试工程需要使用函数时(例如,并不需要使用29种所有的RSendAsMessage方法)。空函数法类似于以下几行实现链接的代码:
void RSendAsMessage::CreateL(RSendAs &, TUid) {}
TInt CMsvStore::HasBodyTextL(void) const { return KErrNone; }
CMsvStore * CMsvEntry::ReadStoreL(void) { return NULL; }
当所有缺失的方式都被使用之后,目标工程编译并链接。此时测试工程将正常无误运行。
● 实现测试
现在我们开始在实际条件下测试目标。任何有测试前缀的方式可看作是一个由框架执行的测试实例。测试实例被执行之后,该测试框架在调用测试法和tearDown()之前调用setUp()函数。安装时应把测试目标设置成默认状态。测试实例中,只需要在已经实例化的测试目标中运行该方式以证实使用的方式和状态是我们预期的。测试目标和设置时生成的其它源代码需被销毁。我们可按以下方式设置:
void CMapExampleSmsEngineTest::setUp()
{
iObserver = new (ELeave) DummyObserver();
iTarget = CMapExampleSmsEngine::NewL(iObserver);
}
在构造过程中驱动器需要一个观察器。对参数可使用伪排除(查看附录A的详细介绍)。释放资源时执行销毁。
void CMapExampleSmsEngineTest::tearDown()
{
delete iTarget;
delete iObserver;
}
首先应拿来测试的实例是短信发送。这是个很简单的过程:该函数被调用而且如果他没有退出(抛出异常)那么这个测试用例就通过了。
void CMapExampleSmsEngineTest::testSendMessage()
{
iTarget->SendSmsL(_L("12345678"), _L("abcd"));
}
短信发送可能会失败。RSendAsMessage::SetBodyTextL()可被退出以模拟异常。然后执行测试实例来确保SendSmsL退出。尽管如此,仅在测试实例里SetBodyTextL才能退出而且测试实例应控制该过程。实现控制的一种方法是运用全局变量,在调用测试目标时设置该测试实例,然后在变量的基础上实现SetBodyTextL。另一种更普遍的方法是定义一个全局函数指针,该指针在定义时被SetBodyTextL调用。测试实例代码如下:
2 void (*gRSendAsMessage_SetBodyTextLHook)() = NULL;
3
4 void ThrowExceptionL()
5 {
6 User::Leave(KErrGeneral);
7 }
8 void RSendAsMessage::SetBodyTextL(const TDesC16& a)
9 {
10 if(gRSendAsMessage_SetBodyTextLHook)
11 gRSendAsMessage_SetBodyTextLHook();
12 }
13
14 void CMapExampleSmsEngineTest::testSendMessageExceptions()
15 {
16 gRSendAsMessage_SetBodyTextLHook = ThrowExceptionL;
17 TS_ASSERT_THROWS_ANYTHING(
18 iTarget->SendSmsL(_L("12345678"), _L("abcd"))
19 );
20 }
21