现在,许多游戏项目要么跳票严重,要不就是发布时Bug多多。当然,这样的现象并不仅存于游戏工业。例如,根据2001Standish集团发表的那份 声名狼藉的报告“极度混乱”所表述的,70%以上的软件项目要么被取消,要么严重的超时和超支。然而,游戏是软件开发复杂性的最佳代表,不同技能的人需要 协同工作,这也就是某些人所说的游戏项目中高风险因素所在。
软件项目延期、Bug满天飞和失败的原因是多种多样的,但看起来除了随产品特性不断变化之外,测试和品质管理是永恒的问题。以我们的经验来看,相当多数的游戏开发工作室完全依靠人工的方式来测试游戏引擎、开发工具和游戏代码,几乎没有采用自动化过程测试。很巧,在2002GDC的圆桌会议:游戏中的纯软件工 程,只有18%的与会者表示他们参与的项目采用了自动化测试。
在2000年,我们的客户,当时新成立的中间件公司对我们的3D引擎的稳定性和大量的BUG抱怨频频,我们第一次想到了自动化测试。直到那时,每当完成一 个新特征,我们还是依靠人工测试,并且使用这些特征开发出技术演示供市场部使用。我们在彻底分析了情况后得出以下结论,我们的软件质量问题主要和我们测试 方法有关:
*人工测试不够全面和彻底,因为它仅仅花费了很多时间。 代码在修改或添加之后,它本应运行预定义的人工测试集来保证修改不会产生新的问题。人工测试花费的时间越来越多,并给开发者带来挫折感,打击他们执行测试 的积极性。而且,测试的工作量使得开发者不愿意改进或优化现有的代码。
*当开发者测试他们自己的代码时,他们总是不愿意(潜意识?)执行最苛刻的测试用例,因此就导致了最有可能出错之处也是最不可能被全面测试到这样的情形。
因此,我们决定采用自动化测试,从开发一个新SDK部件开始。结果是鼓舞人心的,最终我们把它推广到所有的SDK部件开发中去。测试框架极限编程,由Kent Beck和Martin Fowler总结的一系列方法和经验,带来了自动化测试的流行。一般来说,自动化测试指无需用户干预,用来验证软件产品中的功能子集的代码和数据。它可以是用来测试某个特定类方法(通常称为单元测试),也可以是用来测试程序功能性的集成测试(功能测试)。
为了促进自动化测试进程,有许多开源代码的单元测试框架,比如CPPunit(C++专用)或Nunit(.Net专用)。这些测试框架提供了GUI来 运行测试集并提供测试结果反馈。根据你的项目,也许需要根据你的游戏进行一些额外的功能扩展和自定义,例如支持跨平台。这些测试框架的内容,一个单元测试 对应一个函数,测试类由多个单元测试组成,并且包含一个开始和结束测试的方法(例如载入和卸载一幅地图)。这些测试类可以放在分离的执行文件中,例如 DLL文件,也可以与主项目在一起。除此之外,测试类应该存放在产品代码之外的文件中,这样的话,他们就可以很方便的从版本发布中移除。
物理引擎的简单测试代码,如果任何一个VTEST条件没有满足,那么测试就失败。该测什么?当要决定测试的范围时,实用第一。一般来说,为简单的功能编写单元测试是没有意义的,比如常见的getter和setter方法。为了让自动化测试物有 所值,被测试的代码至少应该是可能会产生错误的,比如,发射一束穿透游戏场景的光线并且返回它穿过的任何几何物体的方法(光线测试),然后将返回的结果与 编写测试用例的作者提供的预期结果作比较。
到底是只为类的公用 接口编写测试用例(黑盒测试)还是要兼顾类的私有成员(白盒测试),是一个有争议的问题。通常来说,黑盒测试比白盒测试粗糙,它们只能检查一个操作的最终 结果,不能检查内部中间状态,它们对于被修改的测试代码比较迟钝。刚才提到的光线测试功能可能被全部重写(比如原先的版本运行效率不够),但是它返回的结果没有变化。这时,白盒测试用例就需要跟着重写,然而黑盒测试可以继续用来检测代码修改后,所产生的结果是否与原先一致。
因此,我们认为自动化测试中,测试范围只要包括类的公有成员就够了,毕竟,类的内部修改比它接口修改要频繁得多。
特别是在游戏开发领域,大多数情况下,把测试结果和用例编写者提供的数据手工作比较是不太现实的。例如,检测与复杂的几何体碰撞的交点,人工提供相关测 试数据几乎不可能。相反,将测试结果与早期代码产生的结果数据相比较,被称为“回归测试”。用例编写者可以评审参考数据,例如,使用简化图形的碰撞物体,如果被证实是正确的,它就可以一直用于测试。这样,自动化测试可以帮助你确认新代码产生的结果与原先的一致。
代码功能测试会生成非常复杂的输出数据,比如游戏的图形渲染引擎,回归测试是唯一可行的自动化测试。以图形渲染引擎为例,所有图形测试以输出最终平台相关的图形文件为结果。一旦自动化测试开始运行,渲染出来的图形文件与样本图形文件逐一像素的进行比较,如果有差异,那么测试失败。为了减少样本图形文件的存占用,你可以使用图形快照来进行测试。
图形回归测试的优势在于即使是肉眼难以发现的微小差异也不会被漏掉。除非人们对这个场景非常熟悉,否则很难会有人注意到场景中缺失的一个阴影或一个物体或者某个光源的R值与B值被错换了。而回归测试就不会放过任何一个这样的错误。
必须注意到,任何情况下,回归测试的样本数据都是自动生成的。样本数据也许是平台相关,特别涉及到渲染输出的时候,因此,它也许要被生成多次,即使是这样,当渲染通道发生变化导致生成的图形文件有所改变,样本数据也要重新生成。为了不打击开发者编写回归测试的积极性,要做到只需点击框架用户界面上一个按钮就可以重新生成新的参考数据。
如何把所有的整合在一起
包括游戏在内的所有应用,完整的测试集合包括单元测试和回归测试。单元测试适合于测试底层功能性、基础库文件和平台类库。上层的各种功能特征集成测试可以使用回归测试。根据结果,你可以有选择的重构或优化你的逻辑或引擎代码,回归测试一旦失败,你会马上发现出问题的地方,单元测试失败可以让你精确的定位出错之处。
知道代码被你编写的自动化测试覆盖得范围是非常有好 处的,你可以使用代码覆盖率调查工具(BullseyeCoverage/AQtime)。代码覆盖率分析会告诉你,你的代码哪些被调用,也可以提示你测 试集合中的疏漏之处。测试覆盖率应该是多少,无法精确定量,尽管它取决于被测试的代码。细小的方法无需测试,调试用的函数也不必测试。并且,几乎所有的项 目都会包括一些“死”代码,也就是不会被调用到的代码,那么,这些代码自然也不用测试。总而言之,现实中,我们见过的使用自动化测试的游戏和中间件项目中 测试覆盖率大致是55%到70%。