本文是一篇对诺基亚官方文档《S60 Platform: How to Develop Unit Tests》的非正式简体中文翻译版本。希望本文对提高中文开发者社区的程序开发质量带来一些帮助:)
概述
本文档是一篇实用指南,它介绍了单元测试、设计单元测试的技术以及在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。
#ifndef TESTHEADER_H
#define TESTHEADER_H
#include "TestSuite.h"
// forward declaration
class CMapExampleSmsEngine;
class MSmsEngineObserver;
class CMapExampleSmsEngineTest : public CxxTest::TestSuite
{
public:
CMapExampleSmsEngineTest(const TDesC8& aSuiteName) :
CxxTest::TestSuite(aSuiteName){}
private: // from CxxTest::TestSuite
virtual void setUp();
virtual void tearDown();
public:
void testParseMsgCoordinates();
void testParseMsgRequestType();
void testParseMsgUid();
void testSendMessage();
void testSendMessageExceptions();
private: // data
MSmsEngineObserver* iObserver;
CMapExampleSmsEngine* iTarget;
};
#endif // TESTHEADER_H
注: 用来从头文件生成测试套件的Perl脚本,需要类的定义行和构造函数行之间,没有任何换行符号。
- 4.在TestSource.cpp文件中创建空执行函数
#include "TestDriver.h"
#include "Logger.h"
void CMapExampleSmsEngineTest::setUp(){}
void CMapExampleSmsEngineTest::tearDown(){}
void CMapExampleSmsEngineTest::testParseMsgCoordinates(){}
void CMapExampleSmsEngineTest::testParseMsgRequestType(){}
void CMapExampleSmsEngineTest::testParseMsgUid(){}
void CMapExampleSmsEngineTest::testSendMessage(){}
void CMapExampleSmsEngineTest::testSendMessageExceptions(){}
注: TestDriver.h是在编译过程中由TestHeader.h生成的。
- 5.为测试创建最小的Symbian makefile指令: SymbianOSUnit.mmp:
// test class definitions & implementations
USERINCLUDE .
SOURCEPATH .
SOURCE TestSource.cpp
// test target class definitions & implementations
USERINCLUDE ..\inc
SOURCEPATH ..\src
// SOURCE CMapExampleSMSEngine.cpp // Our tests don’t test actual class yet
// libraries the test target depends on
LIBRARY etext.lib
// include SymbianOSUnit mmp file from proper
// directory depending on relative path and target platform
#include "..\..\SymbianOSUnit\SymbianOSUnitApp\group\s60_3rd\SymbianOSUnit.source"
注: CMapExampleSMSEngine源代码被注解停用是因为我们的测试工作还未开始,并且那些组件与其它类相互联系,这使我们需要在目标被测试的时候着意解决的。
- 6.为测试工程创建一个bld.inf文件:
PRJ_MMPFILES makefile ExtraTestBuildTasks.bldmake 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调用。测试实例代码如下:
// global function pointer
void (*gRSendAsMessage_SetBodyTextLHook)() = NULL;
void ThrowExceptionL()
{
User::Leave(KErrGeneral);
}
void RSendAsMessage::SetBodyTextL(const TDesC16& a)
{
if(gRSendAsMessage_SetBodyTextLHook)
gRSendAsMessage_SetBodyTextLHook();
}
void CMapExampleSmsEngineTest::testSendMessageExceptions()
{
gRSendAsMessage_SetBodyTextLHook = ThrowExceptionL;
TS_ASSERT_THROWS_ANYTHING(
iTarget->SendSmsL(_L("12345678"), _L("abcd"))
);
}
该测试实例首先为此函数定义一个会退出函数指针,然后调用SendSmsL。此调用在测试宏(assert macro)内封装。该宏会检测微囊代码抛出异常。如果不这样做,系统会向测试框架报错测试没有通过。
代码覆盖
代码覆盖工具被用来查找测试覆盖测试码的优劣程度。BullseyeCoverage [5]是Symbian开发中最常用的。其使用过程如下:
- 在BullseyeCoverage(查看图3)下选择按钮打开覆盖编译器。
- 重新编译测试程序。
- 在模拟器上运行测试程序。
- 查看覆盖结果。
图3:一轮测试之后的代码覆盖情况
测试结果包括函数覆盖和分枝覆盖。从如上图3所示,我们能看到SendSMSL()被充分测试(从结构化角度来看),而ParseMsgUid()则被部分测试。使用更详细的覆盖试图(查看图4),代码内容将会被显示,并且如果所有的目标路径都没被全部执行的话,它将会被标注出来。
图4:详细的代码覆盖分析
将科覆盖率提高到100%是非常困难的。通常的方法是使测试实例尽可能小而简单。这就是为什么很多测试实例直到目标函数的覆盖率达到可接受程度时才被用到。测试实例需通过函数的期望值测试,直到所有有意义的小目标在执行中均被测试。目标对象的状态在从测试代码里调用实函数时或许需要在外部被修改。这个过程可通过以下方式实现:
- 当它们为公有时直接改变其属性调用能改变状态的必要函数时(例如存在所需属性的设置函数)
- 间接改变其属性当目标类的属性为保护类或私有类时,我们需要一种特殊的方法,比如
- 把一个测试类定义为一个友元类,例如:
class CMapExampleSmsEngine : public CBase,
public MMsvSessionObserver
#ifdef __SYMBIANOSUNIT
{
friend class CMapExampleSmsEngineTest;
#endif
...
}
由测试目标类衍生一个封装类,并对其使用可在实类里成为保护类属性的函数。测试时在类里或者类函数里使用预编译,使其提供更多灵活的类属性方式。注意代码会因为预编码时存在的比较差的可读性而出现不同的断点甚至引起很难处理得新的错误。
接下来的一章将会介绍理论上的单元测试并提供开发单元测试时需要牢记得策略和技巧。
理解可测试性
是什么决定软件是可测试的还是不可测试的呢?通过检查现有的函数(如上一章介绍的),我们很容易可以看出在程序中添加测试程序的早晚将会产生不同的效果,如果程序的内部结构是复杂的,开发者就不必存取数据、函数和事件处理系统(当使用异步接口时)。
Pressman[7]定义了以下几条使软件更具测试性的原则:
可操作性:软件对象越易操作,就越具有可测试性。所以在默认状态下,尽可能不让代码过于膨胀冗余,及早地去除bug(在它们让你的程序死掉之前)。
可观察性:所见即所测。缺失的源代码或是文档将会让我们很难判断如何测试对象。不可实现的属性也将使我们很困难甚至不可能确定结果是否正确。异常和错误条件以及它们的输出都应该是有根据的以便于我们能判断我们的操作是否是我们想要的。
可控制性:尽可能保证函数开源以便测试能够很容易地控制测试对象。实际上,较长的代码缺失和复杂的函数使它们成为共有类或保护类。软件使用的数据需是测试程序中可实现可改变的部分。
可分解性:通过控制测试范围分割问题。软件是由很多小段代码组成的,这些小段代码可拿来独立测试。实际上,一个测试实例应测试一个类,所有的依赖性都应该能被小段代码和模拟对象所替代。
简易性:代码越少,要用的测试越少。系统里不应该存在没有用到的功能函数(删除死码)。软件结构应该是简单且模块化的。代码应该尽可能短,应具有可读性,不存在任何没用的控制架构。
稳定性:改动越少意味着测试所受的干扰越少。软件的改动应该是有计划可控制的。设计(和测试)的软件是用来将程序从运行失败状态恢复正常。Pressman建议缩减代码和测试的改动,然而那些思维活跃的开发者更喜欢不停地反复修改(改动代码,测试并删除不必要的代码)。
可理解性:测试者拥有的信息越多,测试效果越好。一般的文档已经足够了。各组件相互依赖以及内部和外部组件的使用,应该是清晰明了的。基于代码的所有改动都应该是可交流的(改动时使用版本管理系统和可视化文件比较器是比较明智的)。
开发单元测试
开发者是如何知道应创建的测试类型以及何时测试呢?这个问题没有明确的答案。我们要知道的是开发过程中单元测试是最经常被用到的,另外它的目的是在开发初期尽可能地减少bug,从而减少测试量并确保在其在高级测试中的正确性。将软件分割成单元来测试也迫使开发者创建更优化的软件构架,这能有助于查找bug和软件维护。
测试法可分为行为测试和结构化测试。行为测试确保程序的行为符合设计的意图。例如,当网络数据流不适用目标缓冲器时该方法使其成为可能。结构化测试在另一方面确保所有重要的控制路径的传输都被测试过。
实际上,首先创建行为测试其次度量科覆盖是非常有用的。然后在结构分析的基础上添加测试实例直到实现必要的覆盖。
这章我们将详细介绍普遍的测试技术。
黑盒测试对白盒测试
黑盒测试是一种通过在测试环境下不需要任何内部知识知识来检查对象的方式。只需检查输入和输出。
这种测试法的不足之处是我们很难确保所有的路径都被测试到。尽管如此,对于那些大型的复杂系统,依然更倾向于使用黑盒测试简化事情的方法。
另一方面白盒测试的目的是要运用尽可能多的内部相关技术知识为对象进行测试。它允许测试者选择能使所有(或最重要的部分)路径都能被测试的测试输入方式。路径可由控制构架和获取数据来构建。代码覆盖工具自动运行来寻找路径并报告它们的测试的进展如何。详细内容可查看Wikipedia [6]。
自从白盒测试反映对象的内部工作组以来,当测试发生改动时,它就需要升级。而黑盒测试只需在方法签名和语义发生变化时才被修改。
行为测试技术
一种非常普遍的代码错误是对边界问题进行不适当的操作。边界值分析的观点是指在边界区域进行测试。例如,MIN, MIN-1,MAX, MAX+1。这些值在(当使用)输入和输出函数时都应被检验。
我们不可能创建所有可能的系统会用到的函数输入数据,尽管如此,相似的数据被函数使用时并不改变其执行路径,所以这些数据可被抽象化。等价类分割就是被设计用来把所有的相似输入值划分成类的行为测试。例如,如果函数接受-5到15的值,我们就只需要三个测试部分:
- 有效: [-5 – 15]
- 无效: [-n – -6]
- 无效: [16 – n]
特殊值是错误产生的一个重要原因,它们需要函数额外注意,比如以下例子:
- 0和1的算术运算和函数使用
- 90度和多重复合
- 空字符串
- NULL值
经验和直觉能指引开发者预测代码错误的可能性。因为程序经常产生相似错误,通过错误猜想能使测试用例的创建更加实际有效。特别是当软件和开发组有进展时这是个很实际的方法,而且测试套件随之逐渐运行。如果有改动,这样的测试能标记出软件代码中充斥的bug。
相关错误猜想包括:
- 不适当或缺失的错误操作,例如,用RFile.Open()代替User::LeaveIfError(RFile.Open()).
- 不适当的异常操作,例如,捕获异常而忽略问题(当产品中存在异常时,它将使软件死掉)。(堆或其他资源)代码泄露,正确使用cleanup stack 是一种为自动变量及类变量的构造和析构提供的很好的方法。当在指针后面删除对象时,这些指针在进行任何(可能产生退出的)操作时都应被置NULL直到在析构函数里被删除。
- 不恰当的语义转换。例如,在函数里被分片写在文件里的数据首先是调用而不是集合所有所需数据,然后将其写在一个原子操作里。如果操作中发生异常则第一种方法很容易破坏该文件。
- 不恰当地使用CleanUpStack和CActive。
- 无效的事件处理,例如,当事件按不同顺序发生时会有什么结果?通常的办法是设计一个有限状态机,然后简单地实现这个状态机。
- 对象的生命周期由其所处的状态不同而存在差异,例如,当参考对象不期望时(如果没有正确考虑或引证,调用返回值以及产生这种状况的活动对象)参对象被删除。
- 多线程, 并行执行和死锁。
结构法
代码覆盖[14]的过程如下
- 在程序中寻找不被测试实例测试过的区域;
- 创建附加测试实例以增加覆盖范围;
- 为代码覆盖确定一个定量测量,即一种非直接方式的质量。
理解代码覆盖分析不能识别缺失代码是很重要的。例如,缺失的功能函数。这就是为什么结构测试函数绝不能用作唯一的测试技术。结构测试是行为测试的一种补充,而非取代。
结构覆盖方式有很多,包括:
- 引用覆盖
- 判定覆盖
- 条件覆盖
- 多条件覆盖
- 条件/判定覆盖
- 修正条件/判定覆盖
- 路径覆盖
每一种覆盖方式都有其利弊,BullseyeCoverage解释了条件和判定覆盖,及它们不存在特定方式缺陷的简易的优势。更多有关覆盖方法和测试技术,更多查看[9], [10], [11], 和 [14]。
使用存根和伪对象
Martin Fowler的"伪对象不是存根"[15],是阐述存根和伪对象差别的很好地文章。它们共有的特点是测试对象所依赖的依赖性被给于执行反馈的应用性所取代,并提供从测试执行中转换运行环境的可能性。Fowler 从以下方式定义了伪对象:
Mock Objects术语已成为一种流行的说法,它描述了测试用的模拟实对象的特殊实例对象。如今许多语言环境都有其自己的框架,这使得我们很容易创建伪对象。尽管如此,最不常被我们意识到的是伪对象只是一个特殊的测试对象实例,是一种不同形式的测试。
附录A的示例代码使用的函数,把所有的可执行文件记录到一个巨集_LOGF的文件里。这个巨集可被更改用来往动态的缓冲器里写结果。然后测试实例可以执行测试接着在缓冲器里以正确的指令校验某些被(用正确的存储内容)调用的函数。这种被动的运用替换被称为存根(stub)。
当存根函数的语义能被动态地(从测试实例中)更改时,这种运用更趋向于被叫做伪对象(mock)。当单元测试达到高覆盖时,实际的做法是对每个类都进行伪对象(mock)操作。因为实类常相互参考,测试实例可选择用哪个例子替换伪对象或是在哪里使用具体对象。
jMock是一个Java™ 语言的资料库,它支持带有伪对象(mock)的java代码的测试驱动开发。这项实践是非常有趣的,但用c++的实现一些应用是很困难的,因为所有的东西都需要从头开始。"Mock Roles, Not Objects" [17] 一文是非常值得阅读理解伪对象(mock)相关的内容。
其他的技术和工具
复习查看一下代码手册是对付软件本地bug最有效的方法。两人合作是另一种方法,两种处理问题的方式会让bug无地自容。
另外有一些可供扫描代码报告错误或代码中不合理之处的工具,包括:
- LeaveScan (Carbide C++ 1.2随机附带工具)
- PC-linthttp://www.gimpel-online.com/OnlineTesting.html
进阶阅读
诺基亚论坛[12]提供了很多实用性、有测试资源的链接。 维基百科[6]提供了很好的总体阐述和一些测试学科的学术背景资料。
经典的参考书包括《软件测试技术》(Software Testing Techniques)[10]和《软件测试全面向导》(The Complete Guide to Software Testing)[9]。Software Engineering – A Practitioner’s Approach [7] ,如其名字所示,提供了有用而浅显易懂的信息。聪明的开发者能从中受益,但是如今理论以开始变得不那么重要了。
《S60智能手机的质量监测》(S60 Smartphone Quality Assurance)[11]一书对所有工作在S60 平台的人来说是一本非常有帮助的新书。Symbian OS C++ for Mobile Phones和一些其他的Symbian领域的书籍也同样值得阅读。
参考
[1] S60平台:地图和位置范例程序, 诺基亚论坛
[2] SymbianOSUnit 官方网站, http://www.symbianosunit.co.uk/
[3] SymbianOSUnit 下载网站 http://sourceforge.net/projects/symbianosunit/
[4] EUnit专业版, http://www.digia.com/C2256FEF0043E9C1/0/405001166
[5] BullseyeCoverage, http://www.bullseye.com/
[6] Software Testing(软件测试), 维基百科, http://en.wikipedia.org/wiki/Software_testing
[7] Software Engineering – A Practitioner’s Approach 《软件工程》, 第四版, 作者:Roger S. Pressman, 出版社:McGraw-Hill, (1997)
[8] Symbian OS C++手机应用开发, 中文版,第二卷 (译者注:中文版翻译质量不高,英文版在这里), 作者:Richard Harrison, 出版社:Wiley, (2004)
[9] The Complete Guide to Software Testing, 第二版, 作者:Bill Hetzel, 出版社:Wiley, (1988)
[10] Software Testing Techniques, 第二版, 作者:Boris Beizer, Van Nostrand Reinhold, (1990)
[11] S60 Smartphone Quality Assurance, 作者:Saila Laitinen, 出版社:Wiley, (2006)
[12] 全方位关于产品质量方面的信息, http://www.developer.nokia.com//Design/Design_process/Getting_started/Testing_usability_and_evaluation.xhtml
[13] EUnit, 强大的Erlang单元测试框架, Richard Carlsson, Mickaël Rémond, [--hamishwillee:I removed broken link to "http ://user.it.uu.se/~richardc/eunit/EUnit.ppt"]
[14] 代码覆盖分析, http://www.bullseye.com/coverage.html
[15] "Mocks Aren't Stubs," http://www.martinfowler.com/articles/mocksArentStubs.html
[16] jMock - 轻量级Java伪对象(Mock Object)库, http://www.jmock.org/
[17] "Mock Roles, Not Objects," http://www.jmock.org/oopsla2004.pdf
附录A TestSource.cpp
#include "TestHeader.h"
#include "TestDriver.h"
#include "Logger.h"
#include "cmapexamplesmsengine.h"
#include <msvstore.h>
// ========== logger ==========
#include <flogger.h>
#define __DEFINE_LITERAL(aLiteralName, aStr) _LIT(aLiteralName, aStr);
_LIT( _KLogDir, "MyLogs" );
_LIT( _KLogFile, "test.txt" );
#define _LOGF( aEllipsis )\
{\
_LIT(_KFormat,"%S(%d):%Ld:%S: ");\
__DEFINE_LITERAL( _KFile, __FILE__ );\
TPtrC8 _func8((TUint8*)__FUNCTION__);\
TBuf<40> _func;\
_func.Copy(_func8.Right(40));\
TBuf<256> _log;\
_log.Format(_KFormat, &_KFile, __LINE__, RThread().Id().Id(), &_func);\
_log.AppendFormat aEllipsis;\
RFileLogger::Write( _KLogDir, _KLogFile, EFileLoggingModeAppend, _log );\
}
#define _HERE() _LOGF((KNullDesC))
// ========== stubbed / mocked implementations ==========
class DummyObserver : public MSmsEngineObserver
{
virtual void MessageSent()
{
_LOGF((_L("DummyObserver::MessageSent()")));
}
virtual void MessageReceived(TDesC& aMsg, TDesC& aAddr)
{
_LOGF((_L("DummyObserver::MessageReceived(%S, %S)"), &aMsg, &aAddr));
}
virtual void MessageRequested(TDesC& aMsg, TDesC& aAddr)
{
_LOGF((_L("DummyObserver::MessageRequested(%S, %S)"), &aMsg, &aAddr));
}
virtual void SmsEngineError(TInt aErrorCode)
{
_LOGF((_L("DummyObserver::SmsEngineError(%d)"), aErrorCode));
}
};
void RSendAsMessage::AddRecipientL(const TDesC16& a, RSendAsMessage::TSendAsRecipientType b)
{
_LOGF((_L("RSendAsMessage::AddRecipientL(%S, %d)"), &a, b));
}
void RSendAsMessage::Close()
{
_LOGF((_L("RSendAsMessage::Close()")));
}
void RSendAsMessage::CreateL(RSendAs &a, TUid b)
{
_LOGF((_L("RSendAsMessage::CreateL(%d, %d)"), &a, b));
}
void RSendAsMessage::SendMessage(class TRequestStatus &)
{
_LOGF((_L("RSendAsMessage::SendMessage()")));
}
// global function pointer
void (*gRSendAsMessage_SetBodyTextLHook)() = NULL;
void RSendAsMessage::SetBodyTextL(const TDesC16& a)
{
_LOGF((_L("RSendAsMessage::SetBodyTextL(%S)"), &a));
if(gRSendAsMessage_SetBodyTextLHook)
gRSendAsMessage_SetBodyTextLHook();
}
CMsvEntry * CMsvEntry::NewL(CMsvSession &, long, TMsvSelectionOrdering const &)
{
_LOGF((_L("CMsvEntry::NewL()")));
return NULL;
}
CMsvSession * CMsvSession::OpenAsyncL(MMsvSessionObserver &)
{
_LOGF((_L("CMsvSession::OpenAsyncL()")));
return NULL;
}
CMsvStore * CMsvEntry::ReadStoreL(void)
{
_LOGF((_L("CMsvEntry::ReadStoreL()")));
return NULL;
}
TInt CMsvStore::HasBodyTextL(void) const
{
_LOGF((_L("CMsvStore::HasBodyTextL()")));
return KErrNone;
}
TInt RSendAs::Connect(void)
{
_LOGF((_L("RSendAs::Connect()")));
return KErrNone;
}
TMsvSelectionOrdering::TMsvSelectionOrdering(void)
{
_LOGF((_L("TMsvSelectionOrdering::TMsvSelectionOrdering()")));
}
void CMsvEntry::DeleteL(long)
{
_LOGF((_L("TCMsvEntry::DeleteL()")));
}
void CMsvEntry::SetEntryL(long)
{
_LOGF((_L("CMsvEntry::SetEntryL()")));
}
void CMsvStore::RestoreBodyTextL(CRichText &)
{
_LOGF((_L("CMsvStore::RestoreBodyTextL()")));
}
// ========== test suite ==========
void CMapExampleSmsEngineTest::setUp()
{
_HERE();
gRSendAsMessage_SetBodyTextLHook = NULL;
iObserver = new (ELeave) DummyObserver();
iTarget = CMapExampleSmsEngine::NewL(iObserver);
}
void CMapExampleSmsEngineTest::tearDown()
{
_HERE();
delete iTarget;
delete iObserver;
}
void CMapExampleSmsEngineTest::testParseMsgCoordinates()
{
_HERE();
}
void CMapExampleSmsEngineTest::testParseMsgRequestType()
{
_HERE();
}
void CMapExampleSmsEngineTest::testParseMsgUid()
{
_HERE();
iTarget->ParseMsgUid(_L("REQ E01FF1Cd"));
}
void CMapExampleSmsEngineTest::testSendMessage()
{
_HERE();
iTarget->SendSmsL(_L("12345678"), _L("abcd"));
}
void ThrowExceptionL()
{
_HERE();
User::Leave(KErrGeneral);
}
void CMapExampleSmsEngineTest::testSendMessageExceptions()
{
_HERE();
gRSendAsMessage_SetBodyTextLHook = ThrowExceptionL;
TS_ASSERT_THROWS_ANYTHING(
iTarget->SendSmsL(_L("12345678"), _L("abcd")));
}
附录B EUnit专业版主要功能
- 高级测试创建向导
- 从源代码创建测试要点
- 自动的存根(stub)和适配器创建
- 命令行支持
- 多测试环境支持
- 测试参数支持
- 为资源检查级别设置项目
- 扩展API
- 从测试代码中的任意地方输出任意文本信息
- 内存分配测试
- 处理修饰符(decorator)
- 自动化的内存泄漏检查
- 在测试运行之外监测测试
- 两种测试监测模式
- 处理Panic,异常和leave
附录C 关于单元测试, 测试驱动设计方法(TDD), 测试框架
来自参考[13]:
什么是单元测试?
- 在被隔离的条件下测试"程序单元"
- 函数,模块,子系统等
- 测试特别的行为(或对象)
- 输入/输出
- 压力测试/响应
- 条件变化
单元测试不包括什么
- 单元测试并不包括:
- 性能测试
- 可用性测试
- 系统测试
- 等等
- 单元测试无法取代下面这些,但是在它们中起到了重要的作用:
- 回归测试
- 集成测试
测试驱动设计方法
- 在程序开发的时候就编写单元测试(并且经常运行他们),而并不是在开发完成之后才进行测试。
- 在一个功能编写之前就为之编写测试。
- 在一个功能的所有测试都通过了以后,再去开发另外一个功能。
- 对产品开发效率和专注程度很有帮助:
- 集中解决一个阶段内应该解决的问题
- 避免在规范之外新增问题,并且可以提早进行优化
- 很大程度节省回归测试
单元测试框架(framework)
- 可以很容易地:
- 编写测试: 减少代码编写工作量
- 运行测试: 只需要点一个按钮
- 查看测试结果: 及时了解效率和反馈
- 从Beck和Gamma为Java设计的JUnit框架开始流行