加强警戒(En garde)!
要记住,您的客户对您的产品有与您不一样的想法。他们会在一个您的小组很可能从来也没想到的 —— 或者至少是没有可能测试的 —— 环境中安装它。他们会以您从来没有想到过的方法使用它,并以您意想不到的方法配置它。下面的列表有助于帮助您保证他们不会发怒:
验证所有收到的参数的完整性(考虑如果您期待一个数组而传递来的是一个 null,但是您在索引数组之前没有检查这种可能性时会发生什么情况)。
考虑所有可能的错误情况并增加处理每种情况的代码(您希望代码得体地处理错误条件而不是堵塞它)。
对于那些未预料到的错误条件,加入一个一般性的“捕获所有”错误处理程序。
在适当的时候和地点使用常量。
在代码各处加入跟踪和日志。
如果您的产品将翻译为另一种语言,那么保证您的代码可以“支持”它。即使出现这种情况的机会很小,但是提前计划总是好一些。修改代码以使它提供支持是最容易产生缺陷的。下面是几个您要考虑的与支持相关的问题:
您是否有任何硬编码的字符串?
您是否正确地处理不同的日期/时间?
不同的货币表示呢?
还有,在代码中使用大量断言。
给您的代码加上充分的 注释。总之,您还记得在六个月前编写那个方法时的想法吗?一年后要修改您的代码的某个人又会怎么想呢?在我们提出的所有建议中,这一条可能是最重要的。
单元测试(防御性测试技术)
在本文中,我们所说的 单元测试 是开发人员在自己的代码正确编译后、在交给功能测试小组之前进行的所有测试和分析。正如我们在 这只是一个测试 中提到的,主动进行单元测试并 在测试时像一位测试者那样思考(即,必须往坏处想、热衷于破坏并喜欢恶作剧)是很重要的。下面是在单元测试时要记住的几件事。
静态代码分析工具
第一种,也是最容易的分析代码的方法是让别人替您做 —— 或者像在这里一样,让其他 工具 替您做。有一些不同的静态代码分析工具可用,从综合性的工具 —— 一些开发机构实际上在他们的“编译”环境(这可是需要购买的)中加入了这样的工具 —— 到其他可以免费从 Internet 上下载的工具。
发现缺陷
当您准备运行代码并检查缺陷时,要记住往坏处想。这些缺陷是您所创建的或者由您忽略的代码产生。下面是一些帮助您找到代码中缺陷的提示:
试着强行制造您所想到的所有错误条件并检查可以出现的所有错误消息。
试着使用与其他组件或者程序交互的代码路径。如果其他程序或者组件还不存在,那么就自己编写一些脚手架代码以使您可以试用 API 或者填充共享内存、共享队列,等等。并让您的功能测试小组可以使用这个脚手架代码,这样他们就可以把它加入到他们的武器库中。
对于 GUI 中的每一个输入字段,试验下面多种不同的组合(考虑 自动化):
不可接受的字符(控制字符、非打印字符等)。
过多的字符。
过少的字符。
负数(特别是当您只期待正数时)。
过大和/或者过小的数。
剪切和粘贴数据和文本到输入字段,特别是当您编写的代码限制用户可以键入该字段的内容时。
文本和数字的组合。
全大写字母和全小写字母。
为代码创建“压力条件”,如大量活动、慢连接的网络和所有您想到的可以将代码推到极限条件的东西。
反复进行同样的步骤,然后:
检查未预计到的内存损失条件。
检查当内存用光时发生什么。
试图创建缓存溢出、满队列、不可用的缓存以及其他“不能正确工作”的情况。
对于数组和缓存,试着向数组(或者缓存)增加 n 个数据项,然后试图删除 n+1
个项。
关于时间的考虑?
如果操作“b”在操作“a”之前发生会怎么样?就算您 认为 它不会发生 —— 您能 使 它发生吗?如果是的话,可以打赌您的客户会使它发生的。最好现在找出来,而不是在修复成本更高、并听到客户报怨您的软件质量糟糕之后再去做。
脚手架代码
我们在前面 发现缺陷 中讨论了脚手架代码。如果是为自己的使用需要而创建的,一定要将它交给验证工程师。可能您提供的脚手架代码使他们可以很快地开始测试您的代码,或者至少使他们更好地理解当其他组件存在时可以预期什么。
如果您的产品有保护性的安全功能,那么您必须测试它们。提供可以创建您想要防止的情况的脚手架代码是很重要的:您必须能够创建系统试图防止的那种情况。
脚手架代码的另一个简单例子是提供操纵队列的代码。如果您的产品使用队列,那么想像如果有一个可以在运行时从队列中增加或者删除项、破坏队列中的数据(以保证适当的错误处理)等等的工具会有多方便。
源代码级调试程序
使用源代码级的调试程序是进行彻底和成功的单元测试的关键方法。开发人员应该与他们的调试程序共生死。不幸的是,对源代码级的调试程序的充分了解和使用是一种正在消亡的做法,尽管这些调试程序的好处远远超过任何学习曲线。简而言之,我们强烈鼓励您全面学习一种调试程序。下面是用源代码级调试程序对代码进行单元测试的几种方法。您可以:
在运行中操纵数据 —— 例如,在输入代码时设置中断点,然后重新设置传递的参数值以检查代码是否能正确处理(现在为)无效的参数。以这种方式使用调试程序就不需要让错误条件真正发生。
设置断点,然后“单步”通过代码,这样您就可以看到每一行代码所做的事情。
设置对变量的“监视(watch)”。
强制使用错误代码路径。
观察调用堆栈以检查哪一个例程调用了您的代码。
在错误发生时“捕获”它们。
执行边界检查。
认识您的验证工程师
验证工程师是测试知识的很好来源。他们可以给您指出要测试什么并帮助您了解可以在代码中加入什么以帮助他们测试(如代码钩子)。此外,您可以向他们展示如何使用您的脚手架代码。他们还会很有兴趣了解您认为在测试中哪些应该是自动化的 —— 如果您某些事情做了不止一遍,那么他们也会。
开始测验!
现在是进行小测试的时候了。让我们看看您是否用心了。
问题
您希望检查一个整数的值是否为 5。通常,要这样编写代码:
if ( i == 5 ) then
{
//
// do something...
//
}
不过,如果您对代码进行“手指检查”,并且把代码写成了下面这样会出现什么情况呢?
if ( i = 5 ) then
{
//
// do something...
//
}
这个失误是一个缺陷,但是只有在运行时才能捕获它 —— 可能需要相当的调试努力才能找到它。编译器会轻易放过您的代码,那么如何防止这种错误发生?
答案
实际上有两个答案:您可以使用一种上面描述的静态代码分析工具,并希望它有足够的健壮性可以捕获这种错误,也可以交换操作数以使常量位于左边:
if ( 5 == i ) then
{
//
// do something...
//
}
因为这种方法保证您可以在编译代码时立即捕捉到问题,所以它是首选的技术。虽然它看上去有些笨,但是代码可以编译并运行得很好。然而,当您“手指检查”代码时就可以立即看到好处了:
if ( 5 = i ) then
{
//
// do something...
//
}
可是编译器不喜欢这样,因为 5 不能被赋值为另一个值。这就是我们在 前面 说您应当将编译器看成是您的朋友时所表达的意思。
您还可以在检查 null 指针时使用这个技巧。看下面的代码:
if ( returnString == null )
{
//
// do something...
//
}
如果您偶然将它“误写”成下面这样会发生什么呢?
if ( returnString = null )
{
//
// do something...
//
}
您可能不会得到想要的结果。而改用下面的方法您会得到与我们刚描述过的同样的“编译器保护”:
if ( null == returnString )
{
//
// do something...
//
}
结束语
为保持简明扼要我们做了一个相当简洁的归纳:要么现在去做,要么以后花 多得多 的代价去做。换句话说,您在开发周期的早期在测试和预防代码缺陷上花的时间越多,您在以后节省的时间和金钱就越多。这就是防御性编码的意义。它就是这么简单。