【IT168技术】代码覆盖工具被用来查找测试覆盖测试码的优劣程度。BullseyeCoverage [5]是Symbian开发中最常用的。其使用过程如下:
1. 在BullseyeCoverage(查看图3)下选择按钮打开覆盖编译器。
2. 重新编译测试程序。
3. 在模拟器上运行测试程序。
4. 查看覆盖结果。

图3:一轮测试之后的代码覆盖情况
测试结果包括函数覆盖和分枝覆盖。从如上图3所示,我们能看到SendSMSL()被充分测试(从结构化角度来看),而ParseMsgUid()则被部分测试。使用更详细的覆盖试图(查看图4),代码内容将会被显示,并且如果所有的目标路径都没被全部执行的话,它将会被标注出来。

图4:详细的代码覆盖分析
将科覆盖率提高到100%是非常困难的。通常的方法是使测试实例尽可能小而简单。这就是为什么很多测试实例直到目标函数的覆盖率达到可接受程度时才被用到。测试实例需通过函数的期望值测试,直到所有有意义的小目标在执行中均被测试。目标对象的状态在从测试代码里调用实函数时或许需要在外部被修改。这个过程可通过以下方式实现:
* 当它们为公有时直接改变其属性调用能改变状态的必要函数时(例如存在所需属性的设置函数)
* 间接改变其属性当目标类的属性为保护类或私有类时,我们需要一种特殊的方法,比如
* 把一个测试类定义为一个友元类,例如:
2 public MMsvSessionObserver
3
4 #ifdef __SYMBIANOSUNIT
5 {
6 friend class CMapExampleSmsEngineTest;
7 #endif
8 ...
9 }
10
由测试目标类衍生一个封装类,并对其使用可在实类里成为保护类属性的函数。测试时在类里或者类函数里使用预编译,使其提供更多灵活的类属性方式。注意代码会因为预编码时存在的比较差的可读性而出现不同的断点甚至引起很难处理得新的错误。
接下来的一章将会介绍理论上的单元测试并提供开发单元测试时需要牢记得策略和技巧。
理解可测试性
是什么决定软件是可测试的还是不可测试的呢?通过检查现有的函数(如上一章介绍的),我们很容易可以看出在程序中添加测试程序的早晚将会产生不同的效果,如果程序的内部结构是复杂的,开发者就不必存取数据、函数和事件处理系统(当使用异步接口时)。
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随机附带工具)
* CodeScanner
* SymScan
* PC-lint http://www.gimpel-online.com/OnlineTesting.html
* Understand for C++
附录A TestSource.cpp
2 #include "TestDriver.h"
3 #include "Logger.h"
4 #include "cmapexamplesmsengine.h"
5 #include <msvstore.h>
6
7 // ========== logger ==========
8 #include <flogger.h>
9 #define __DEFINE_LITERAL(aLiteralName, aStr) _LIT(aLiteralName, aStr);
10 _LIT( _KLogDir, "MyLogs" );
11 _LIT( _KLogFile, "test.txt" );
12 #define _LOGF( aEllipsis )\
13 {\
14 _LIT(_KFormat,"%S(%d):%Ld:%S: ");\
15 __DEFINE_LITERAL( _KFile, __FILE__ );\
16 TPtrC8 _func8((TUint8*)__FUNCTION__);\
17 TBuf<40> _func;\
18 _func.Copy(_func8.Right(40));\
19 TBuf<256> _log;\
20 _log.Format(_KFormat, &_KFile, __LINE__, RThread().Id().Id(), &_func);\
21 _log.AppendFormat aEllipsis;\
22 RFileLogger::Write( _KLogDir, _KLogFile, EFileLoggingModeAppend, _log );\
23 }
24 #define _HERE() _LOGF((KNullDesC))
25
26 // ========== stubbed / mocked implementations ==========
27 class DummyObserver : public MSmsEngineObserver
28 {
29 virtual void MessageSent()
30 {
31 _LOGF((_L("DummyObserver::MessageSent()")));
32 }
33 virtual void MessageReceived(TDesC& aMsg, TDesC& aAddr)
34 {
35 _LOGF((_L("DummyObserver::MessageReceived(%S, %S)"), &aMsg, &aAddr));
36 }
37 virtual void MessageRequested(TDesC& aMsg, TDesC& aAddr)
38 {
39 _LOGF((_L("DummyObserver::MessageRequested(%S, %S)"), &aMsg, &aAddr));
40 }
41 virtual void SmsEngineError(TInt aErrorCode)
42 {
43 _LOGF((_L("DummyObserver::SmsEngineError(%d)"), aErrorCode));
44 }
45 };
46
47 void RSendAsMessage::AddRecipientL(const TDesC16& a, RSendAsMessage::TSendAsRecipientType b)
48 {
49 _LOGF((_L("RSendAsMessage::AddRecipientL(%S, %d)"), &a, b));
50 }
51
52 void RSendAsMessage::Close()
53 {
54 _LOGF((_L("RSendAsMessage::Close()")));
55 }
56
57 void RSendAsMessage::CreateL(RSendAs &a, TUid b)
58 {
59 _LOGF((_L("RSendAsMessage::CreateL(%d, %d)"), &a, b));
60 }
61
62 void RSendAsMessage::SendMessage(class TRequestStatus &)
63 {
64 _LOGF((_L("RSendAsMessage::SendMessage()")));
65 }
66
67 // global function pointer
68 void (*gRSendAsMessage_SetBodyTextLHook)() = NULL;
69
70 void RSendAsMessage::SetBodyTextL(const TDesC16& a)
71 {
72 _LOGF((_L("RSendAsMessage::SetBodyTextL(%S)"), &a));
73 if(gRSendAsMessage_SetBodyTextLHook)
74 gRSendAsMessage_SetBodyTextLHook();
75 }
76
77 CMsvEntry * CMsvEntry::NewL(CMsvSession &, long, TMsvSelectionOrdering const &)
78 {
79 _LOGF((_L("CMsvEntry::NewL()")));
80 return NULL;
81 }
82
83 CMsvSession * CMsvSession::OpenAsyncL(MMsvSessionObserver &)
84 {
85 _LOGF((_L("CMsvSession::OpenAsyncL()")));
86 return NULL;
87 }
88
89 CMsvStore * CMsvEntry::ReadStoreL(void)
90 {
91 _LOGF((_L("CMsvEntry::ReadStoreL()")));
92 return NULL;
93 }
94
95 TInt CMsvStore::HasBodyTextL(void) const
96 {
97 _LOGF((_L("CMsvStore::HasBodyTextL()")));
98 return KErrNone;
99 }
100
101 TInt RSendAs::Connect(void)
102 {
103 _LOGF((_L("RSendAs::Connect()")));
104 return KErrNone;
105 }
106
107 TMsvSelectionOrdering::TMsvSelectionOrdering(void)
108 {
109 _LOGF((_L("TMsvSelectionOrdering::TMsvSelectionOrdering()")));
110 }
111
112 void CMsvEntry::DeleteL(long)
113 {
114 _LOGF((_L("TCMsvEntry::DeleteL()")));
115 }
116
117 void CMsvEntry::SetEntryL(long)
118 {
119 _LOGF((_L("CMsvEntry::SetEntryL()")));
120 }
121
122 void CMsvStore::RestoreBodyTextL(CRichText &)
123 {
124 _LOGF((_L("CMsvStore::RestoreBodyTextL()")));
125 }
126
127 // ========== test suite ==========
128 void CMapExampleSmsEngineTest::setUp()
129 {
130 _HERE();
131 gRSendAsMessage_SetBodyTextLHook = NULL;
132 iObserver = new (ELeave) DummyObserver();
133 iTarget = CMapExampleSmsEngine::NewL(iObserver);
134 }
135
136 void CMapExampleSmsEngineTest::tearDown()
137 {
138 _HERE();
139 delete iTarget;
140 delete iObserver;
141 }
142
143 void CMapExampleSmsEngineTest::testParseMsgCoordinates()
144 {
145 _HERE();
146 }
147
148 void CMapExampleSmsEngineTest::testParseMsgRequestType()
149 {
150 _HERE();
151 }
152
153 void CMapExampleSmsEngineTest::testParseMsgUid()
154 {
155 _HERE();
156 iTarget->ParseMsgUid(_L("REQ E01FF1Cd"));
157 }
158
159 void CMapExampleSmsEngineTest::testSendMessage()
160 {
161 _HERE();
162 iTarget->SendSmsL(_L("12345678"), _L("abcd"));
163 }
164
165 void ThrowExceptionL()
166 {
167 _HERE();
168 User::Leave(KErrGeneral);
169 }
170
171 void CMapExampleSmsEngineTest::testSendMessageExceptions()
172 {
173 _HERE();
174 gRSendAsMessage_SetBodyTextLHook = ThrowExceptionL;
175 TS_ASSERT_THROWS_ANYTHING(
176 iTarget->SendSmsL(_L("12345678"), _L("abcd")));
177 }
178
附录B EUnit专业版主要功能
* 高级测试创建向导
* 从源代码创建测试要点
* 自动的存根(stub)和适配器创建
* 命令行支持
* 多测试环境支持
* 测试参数支持
* 为资源检查级别设置项目
* 扩展API
* 从测试代码中的任意地方输出任意文本信息
* 内存分配测试
* 处理修饰符(decorator)
* 自动化的内存泄漏检查
* 在测试运行之外监测测试
* 两种测试监测模式
* 处理Panic,异常和leave
附录C 关于单元测试, 测试驱动设计方法(TDD), 测试框架
来自参考[13]:
什么是单元测试?
* 在被隔离的条件下测试"程序单元"
o 函数,模块,子系统等
* 测试特别的行为(或对象)
o 输入/输出
o 压力测试/响应
o 条件变化
单元测试不包括什么
* 单元测试并不包括:
o 性能测试
o 可用性测试
o 系统测试
o 等等
* 单元测试无法取代下面这些,但是在它们中起到了重要的作用:
o 回归测试
o 集成测试
测试驱动设计方法
* 在程序开发的时候就编写单元测试(并且经常运行他们),而并不是在开发完成之后才进行测试。
* 在一个功能编写之前就为之编写测试。
* 在一个功能的所有测试都通过了以后,再去开发另外一个功能。
* 对产品开发效率和专注程度很有帮助:
o 集中解决一个阶段内应该解决的问题
o 避免在规范之外新增问题,并且可以提早进行优化
* 很大程度节省回归测试
单元测试框架(framework)
* 可以很容易地:
o 编写测试: 减少代码编写工作量
o 运行测试: 只需要点一个按钮
o 查看测试结果: 及时了解效率和反馈
* 从Beck和Gamma为Java设计的JUnit框架开始流行