首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

2.4  大型多人游戏中的单元测试

Matthew Walker,NCSoft Corporation

mwalker@softhome.net

元测试(Unit Testing)就是通过编写程序,在一致的条件集下使用另一个程序的功能,并且把获得的结果和期望的结果进行比较。单元是指一个具有内聚性(cohesive)的软件部分,它具有良好定义的接口并且对其他部分的依赖较少。单元测试由一个或多个测试用例(Test Case)组成,这些测试用例会以特定的方式使用单元的功能,并且对结果进行检查。把多个测试用例组合成一个测试集(Test Suite),这样游戏开发人员就能以批处理的方式运行它们。单元测试需要一个或多个特定的数据集来运行被测软件,这些数据集被称为测试数据集(Fixture)。在每次测试运行中使用相同的测试数据集可以保证输入的一致性,这样被测软件的任何行为变化都可以归结为软件的变化,而不是数据的变化。

单元测试已经在软件工程领域应用了很多年,最近更是随着极限编程(Extreme Programming,XP)的发展而流行起来。XP是一种强调“简单(simplicity)、沟通(communication)、反馈(feedback)和勇气(courage)”的软件开发方法[XP99]。单元测试是XP的核心方法之一,它与持续代码整合(continuous code integration)、小型发布(small releases)、结对编程(pair programming)、集体拥有代码(collective code ownership)以及其他实用方法一起,致力于为迅速高效地开发高质量的软件提供服务[Jeffries01]。

2.4.1  为什么MMP游戏需要单元测试

通常单机游戏和那些小型在线游戏的绝大部分利润是在发布后的短时间内获得的。在这类游戏发布后,开发人员很少会对它们再作修改,除非是为了改正那些影响销量的严重错误。与此相反,大型多人游戏往往是一项会在数年时间里持续不断地创造收入的服务。这样的服务在它的整个生命期中需要大量的开发和支持人员。在MMP游戏的生命周期内,为了向订阅者提供新鲜有趣的游戏体验,游戏开发人员需要对代码和游戏内容进行多次修改。在MMP中,一个软件错误就可以让在线服务崩溃或是给游戏平衡带来相当大的麻烦。这些问题威胁着收入的稳定性以及项目的整体成功。对于MMP开发人员来说,单元测试具有两个显而易见的好处:

1.它可以在开发过程中保证代码的完整性,尤其是在整合过程中;

2.在游戏发展过程中,它可以最小化在成品代码中引入错误的风险。

相对传统的计算机游戏来说,MMP产品的长期性以及它们对服务提出的要求使游戏开发人员必须更加重视管理软件开发的风险。单元测试可以让错误更早、更频繁地暴露出来,从而使风险最小化。

2.4.2  单元测试的定义

要设计一个有效的单元测试,必须明确地定义3个主要概念。它们分别是单元(unit)、测试数据集(fixture)和测试本身。

1.单元的定义

所要测试的软件单元必须具有良好定义的接口并且对其他单元的依赖较少。在把所有的依赖条件抽象到单元接口之后,进行测试的代码就没有必要知道它们了。通常,从面向对象的角度来看,一个单元就是一个类。

设计良好的软件通常由互相关联的对象集合和层次组成。定义单元最有效的方法是从最内部、最底层或是最基本的类开始,然后向系统上方进行直到那些高层类被定义。这种方法和软件发展的正常顺序相一致,这可以确保在对那些复杂的代码进行测试之前,它们所依赖的代码已经可以正常工作了。

常见的底层单元有用于创建网络包的数据缓冲(DataBuffer)类、用于并发处理的线程(Thread)类和用于加密的加密(Crypt)类等。高层类则包括那些管理游戏中运行对象的对象管理器(ObjectManager)类、用于分发异步消息的事件管理器(EventManager)类以及用于检测对象是否相交的碰撞管理器(CollisionManager)类等。

2.数据集的定义

每一个单元测试都包含一个或多个数据集,用来为将要测试的单元建立数据集合。一个数据集可以简单到仅仅是一个用来测试数据缓冲类的硬编码字符串,也可以由一组提供给碰撞管理器的具有碰撞几何信息的对象组成。如下就是这两类的数据集:

// test string for the DataBuffer

const char* data = “the quick brown fox”;

// collection of Collider objects

vector<Collider*> colliders;

colliders.push_back(new Collider(0.0, 1.0, 0.0));

colliders.push_back(new Collider(1.0, 0.0, 0.0));

colliders.push_back(new Collider(0.0, 0.0, 0.0));

针对同一个单元的不同测试可能需要不同的数据集。一个对象管理器的正向测试(被设计为应该获得正确结果的测试)可能需要一个对象集合,里面的对象都具有独一无二的标识。而它的逆向测试(被设计为会引起错误的测试)也许需要其测试对象集合中包含指向同一个对象的重复引用以强制错误情况的产生。我们应该定义足够多的数据集以彻底地测试这个单元的各个方面。

通常,数据集可以直接硬编码在单元测试内。这当然是最方便的方法,因为测试人员对于提供给单元的数据具有完全的控制。然而,在测试高层单元时往往会需要具有外部数据的数据集。外部数据是指从文件、数据库、网络套接字或是这个单元测试代码以外的其他资源中读取的数据。之所以使用这种类型的数据集,是为了使被测单元在测试中能够以和游戏实际运行时相同的方式接收数据。

在测试数据集中使用外部数据的难点在于,必须确保这些数据在测试时总是存在,并且在多次测试之后不会改变。这就需要小心地管理测试环境并且把产品数据和测试数据分离开。通常被测单元的开发目标不同于项目目标,这会导致一些不可预测的变化。在测试数据集中使用产品数据会使单元测试受到这些变化的影响。被测单元必须“拥有”它的测试数据以保证其完整和一致。

3.测试的定义

被测单元所有的主要功能都必须使用单元测试。这就意味着要调用这个单元公共接口中声明的方法或函数。游戏开发人员应该对所有衍生数据(根据单元状态计算出的数据)或是改变单元状态的方法进行测试。通常没有必要去测试那些简单地获取或者改变数据的方法,除非它们会返回衍生数据或者导致单元的状态发生改变。

通常是无法对单元的保护或私有接口进行测试的。大多数语言(如C++和Java)所具有的强制访问保护使得测试代码不能访问私有或保护方法。然而这并不会造成什么问题,因为单元测试就是要模拟那些使用被测单元的代码,而这些代码也不能访问那些受限的接口。这里不推荐为了测试的方便去修改那些本来需要限制访问的代码接口。

在对一个单元进行测试时,游戏开发人员必须把调用函数或方法所获得的实际结果和预期结果进行比较。如果实际结果和预期结果一致,测试通过;否则,测试失败。当测试通过时,单元测试代码什么都不用做;当它失败时,单元测试代码应该抛出一个异常,测试框架中的错误报告代码会处理这个异常。预期结果会作为单元测试代码的一部分来进行预先定义,它完全依赖于所使用的数据集和被测代码。

游戏开发人员可以使用多种方法来比较实际结果和预期结果。对于计算所得的数值,我们把它和预期结果进行简单比较就可以了。

// test derived damage value

BattleDrone d;

d.SetWeaponType(PULSE_RIFLE);  // damage=energy*0.4

d.SetEnergyLevel(150);

d.AddDamageBonus(15);

int damage = d.GetAttackDamage();

assert(damage == 75);   // (150 * .4) + 15

对单元内部的状态变化进行测试则更为复杂,最主要的问题在于有时无法访问被改变的状态。当这个状态能通过公有接口访问时这不成问题,正如下面所示。

// test string concatenation

MyString* s = new MyString(“hello”);

s->Append(“ world”);

assert(sÒGetContents() == “hello world”);  

然而,内部状态的改变有时候是通过单元行为的改变表现出来的。在这种情况下,需要编写代码以检测这样的行为改变。下面是一个例子。

// test collision manager

Collider* pC1, pC2, pC3;

pC1 = new Collider(0.0, 0.0, 0.0); // start at origin

pC2 = new Collider(1.0, 0.0, 0.0); // 1 away from c1

pC3 = new Collider(-1.0, 0.0, 0.0); // 1 away from c1

CollisionManager manager;

manager.AddCollider(pC1);

manager.AddCollider(pC2);

manager.AddCollider(pC3);

// first, ensure not colliding

bool bCollided = false;

bCollided = manager.TestCollision(pC1, pC2);

assert(!bCollided);

bCollided = manager.TestCollision(pC2, pC3);

assert(!bCollided);

bCollided = manager.TestCollision(pC1, pC3);

assert(!bCollided);

// move pC2 closer to pC3

pC2.SetPosition(-1.0, 0.0, 0.0);

// test collisions again

bCollided = manager.TestCollision(pC1, pC2);

assert(!bCollided); // still not colliding

bCollided = manager.TestCollision(pC2, pC3);

assert(bCollided);  // this pair is now colliding

bCollided = manager.TestCollision(pC1, pC3);

assert(!bCollided); // still not colliding

在上述例子中,改变一个碰撞(Collider)对象的位置会使它和另一个对象相交,这将导致碰撞管理器(CollisionManager)检测到一个本来不存在的碰撞。

2.4.3  单元测试框架

通过编写程序来使用其他代码的功能并不困难。单元测试通常不过是一个批处理:初始化数据、调用组成测试用例的方法或函数、检测错误。为了便于使用,这些基于批处理的程序通常必须提供一些工具来为每个测试用例重新初始化数据集、把测试用例彼此隔离开、处理预料之中和预料之外的错误、搜集统计信息并且报告结果。

游戏开发人员可以自己编写这些工具,但这没有必要。这些功能虽然是必须的,但其中的大多数都既简单又重复,毫无乐趣可言。目前就有很多设计良好的单元测试框架可以为我们所用。大多数这类框架的实现思想都基于Kent Beck在1994年首次提出的Smalltalk测试框架[Beck94]。Beck把这个框架命名为“Sunit”,这很快就成为实现单元测试常见功能的标准方法。

1.xUnit

人们使用很多语言重写了Beck的框架,包括Java(JUnit)、Python(PyUnit)、C++(CppUnit)和一些其他语言。这些框架被统称为xUnit框架[xUnit02]。它们所提供的工具可以使编写单元测试变得更为简单并且高效。这些框架的实现细节各不相同,但是都实现了和下面类似的接口。

测试用例(TestCase)——这个类是实际测试代码的宿主。单元测试的编写者通常为每个单元测试编写一个测试用例的派生类。每个测试用例的派生类都会实现一个或多个方法来表示一个具体的测试用例。它还实现了一个setUp()方法来初始化数据集以及一个tearDown()方法来做必要的清除工作。这个类的每个实例只会执行它所拥有的测试方法中的一个,这个方法由构造函数的参数指定。需要测试的方法通过标准的run()方法间接调用。run()方法会先调用setUp(),接着调用在构造函数中指定的测试方法,最后调用tearDown()。这可以保证每一个测试用例都能使用一个未经修改的测试数据集。

测试集(TestSuite)——这个类负责把多个测试用例聚合起来,从而把它们当作一个整体来运行。测试人员要为每一个需要运行的测试创建一个测试用例派生类的实例并且把它加入到测试集中去。当单元测试被执行时,测试集会枚举测试用例集合中的每一个实例并且调用它们的run()方法。虽然可以从测试集派生以获得定制的功能,但是通常测试人员会直接使用这个类。

测试结果(TestResult)——这个类负责捕捉测试的运行结果并且记录失败(与预期不一致的测试结果)和错误(预料之外的异常)。测试结果还会为特定的失败和错误记录详细信息,从而帮助测试人员诊断所遇到的问题。

测试运行(TestRunner)——这个类管理所有测试集的批处理运行,它聚合了相应的测试结果,并且会把结果写到指定的输出设备。测试人员可以使用这个类的不同变种来生成不同格式的输出,如文本、XML或是HTML。

2.使用xUnit

Python是一门简单的语言,即使对其一无所知的程序员也能够轻易读懂它。因此,本节使用Python的测试框架PyUnit[PyUnit02]来介绍xUnit的一些特性。本节试图说明那些本质上和语言无关的通用概念。这里所使用的例子可以很方便地转化到CppUnit、JUnit或者其他基于xUnit的框架。

3.实例:对易耗属性(Consumable Attribute)进行测试

下面的例子介绍了对属性(Attribute)类的测试,这个类用于管理角色扮演游戏中的易耗属性,例如健康值和魔法值。这个类包含两个值:当前值(current)和最大值(max)。当前值可以增加或减少,但是永远不能超过最大值。通常,当前值和最大值相等。在当前值减少时,一个时钟会被启动,这个时钟每隔一段时间为当前值增加一定的点数,直到它和最大值相等。最大值也可以增加或减少。如果它减少到一个比当前值还要小的值,当前值也必须作相应的减少。如果最大值增加了,时钟会被启动来逐渐增加当前值。

这个测试是一个独立的Python模块,它的实现在源代码文件attribute_t.py中。这个模块不仅定义了测试类,还提供了初始化代码,这样就可以把这个测试整合到总体框架中去。

# attribute_t.py

# Unit test of the Attribute class.

from unittest import TestCase  # always need this

import time                             # needed for our test

from attribute import Attribute      # unit being tested

#

# Test class for testing Attributes

#

class AttrTest(TestCase):  # derive from TestCase

    # class-level test suite member

    suite = unittest.TestSuite()

    #

    # Fixture definition

    #

    def setUp(self):

        # init with 100 pts current and max value

        self.attr = Attribute(100,100)

        self.attr.SetRefresh(1,5) # 1 pt every 5 sec

    def tearDown(self):

        pass # nothing to do, cleanup is automatic

    #

    # Test methods

    #

    def testChangeCurrent(self):

        # Ensure changes are reflected correctly

        a = self.attr

        a.ChangeCurrent(-50) # decrease 50 pts

        self.assert_(a.current == 50, “Decrease failed.”)

        attr.ChangeCurrent(25) # increase halfway to full

        self.assert_(a.current == 75, “Increase failed.”)

    def testChangeMax(self):

        # Ensure changes to max are reflected correctly

        a = self.attr

        a.ChangeMax(-50) # max reduction reduces current

        self.assert_(a.max == 50, “Max decrease failed.”)

        self.assert_(a.current == 50, “Current > max.”)

        a.ChangeMax(25) # regain max, current stays

        self.assert_(a.max == 75, “Max increase failed.”)

        self.assert_(a.current == 50, “Current grew.”)

    def testRegeneration(self):

        # Ensure depleted current value increases over time

        a = self.attr

        a.ChangeCurrent(-50)

        time.sleep(5)  # regen period of 5 seconds

        self.assert_(a.current == 51, “5 sec failed.”)

    time.sleep(10) # should get 2 more pts

    self.assert_(a.current == 53, “10 sec failed.”)

    a.ChangeCurrent(47) # take us back to max

    time.sleep(10) # should have no more refresh

    self.assert_(a.current == 100, “Excess refresh.”)

#

# Module-level test suite initialization.

# PyUnit framework will call this function.

#

def suite():

    # Add reference to each test method to the

    # TestSuite, so the framework can call it.

    AttrTest.suite.AddTest(AttrTest.testChangeCurrent)

    AttrTest.suite.AddTest(AttrTest.testChangeMax)

    AttrTest.suite.AddTest(AttrTest.testRegeneration)

    # The returned suite will be combined with others

    # by the framework.

    return AttrTest.suite

模块一级的suite()方法使用我们的测试类来初始化一个测试集对象并且将其返回。这个对象是AttrTest类的属性,而不是每个AttrTest实例的属性。这意味着所有的AttrTest实例只有一个测试集。这个测试集包含了一个列表,里面保存了对测试方法的引用。当测试运行时,测试框架会为列表中每一个测试方法创建一个AttrTest实例。每一个实例都会依次调用setUp()、测试方法以及tearDown()。

4.尽可能使用简单的测试方法

注意,本节所用的测试方法都是非常简单的。每个方法只负责某个特定的部分。这样做有两个原因。第一个原因是测试方法越小就越便于阅读和理解。另一个原因是测试人员通过在测试用例的assert_()方法中抛出异常来进行错误报告。(也存在其他错误报告方式,读者可查阅PyUnit的文档以获得进一步的信息[PyUnit02]。)它会停止执行当前正在进行的测试方法,因此这个方法中任何后续的代码都不会被执行。测试方法的数量越多并且规模越小,在测试失败时可以生成的信息也越多。

2.4.4  测试先行的设计

单元测试最有用的应用之一就是测试先行的设计(test-first design)。这个方法强调测试是软件设计的驱动力而不仅仅是一个副产品[Langr01]。

1.在编写代码前编写测试代码

这个方法的关键在于程序员必须在编写任何被测代码前先编写单元测试。这种方法是一个易于采用并且可以快速生成结果的方法。如果测试在第一次运行时就没有任何错误,那么有两种可能:a)代码没有缺陷并且老板给的工资太低了;b)没有真正地使用到他们想要测试的代码。一般,b)是最有可能发生的情况。

测试人员必须确保单元测试确实执行了被测代码并且被测代码能够正确工作。要做到这点,最简单的方法就是为那些他们明知有问题的代码编写测试代码,并且修正那些代码使它们能够正确通过测试。测试人员能够获得有问题代码的最早时间就是在还没有编写代码前。

2.编码的步骤

测试先行的设计按照有规律的步骤进行,如下。

(1)为一个还不存在的函数或者方法编写测试代码。

(2)必要的话,编译测试代码;编译将会失败。

(3)通过编写被测试的函数或方法的存根(stub)版本(译者注:存根版本是指仅包含必要的函数定义和返回值的版本,没有实际功能,仅用来通过编译。)来修正编译错误。

(4)运行测试;因为还未实现功能,测试将会失败。

(5)通过编写被测函数或方法的函数体来修正测试错误。

(6)运行测试;如果失败,修正代码;如果成功,编写另一个测试。

(7)重复上述步骤。

如果按照这个步骤进行开发,就能够以更快、更自然的方式让代码运行起来。更重要的是,在实现设计时游戏开发人员会非常自信,因为他们对每个主要功能都进行了彻底的测试,并且证明它们是可以正确运行的。

3.让自己成为最初的受害者

测试先行的设计强调从使用者的角度编写代码。也就是说,程序员编写他们想要使用的代码,而不是编写他觉得别人可能会使用的代码。他们是功能的作者,同时也是设计决策的第一个受害者。

一个很常见的情况是在亲自使用自己所写的代码之前,他们并没有真正地理解它。有些程序员甚至从来没有使用过自己的代码,而是扔给一墙之隔的质量保证人员或是其他程序员去运行。测试先行的设计,特别是单元测试,通常可以让他们更快地理解自己的设计决策所带来的影响。

4.单元测试和重构(refactor)

随着在代码中加入新功能或是改正错误,游戏开发人员会改变早期的设计决策以适应新的需求。在此,类、模块、方法、函数和数据结构将被分割、合并以及重写,从而使新的设计可以通过一个慎重的有机过程融入到现有的代码中去。

测试先行的设计非常适合在这个过程中使用。这个情况下所使用的模式如下。

(1)辨认出那些需要重构的代码。
(2)编写单元测试来证明现存的代码能够工作。
(3)开始重构,对代码进行微小的、慎重的修改。
(4)重新编译并且对每个修改运行单元测试;单元测试将失败。
(5)重写测试使得它符合新的代码。
(6)重新运行测试,如果代码能够工作,继续重构;如果测试失败,修正新代码。
(7)重复上述过程。

2.4.5  实用因素

把单元测试合并到开发过程中并不只是简单地编写测试。要实现总体的成功,还必须注意测试逻辑,并且应该开发一个可以用于各种不同环境的测试方法。这里,本节对下面这些因素进行讨论。

1.测试过程自动化

单元测试除了可以证明特定的代码单元在最初开发期间能够正确工作,还可以知道对共享代码的改变是否破坏了依赖于它的其他代码。游戏开发人员最好通过建立一个自动化的测试过程来实现单元测试。这个过程通常包含如下几步。

1.从代码库中取出最新的代码。

2.对代码进行编译和链接。

3.运行所有的单元测试。

4.向编程团队报告结果。

这个过程的目标是让代码可以随时通过所有的测试。测试人员应该每天运行几次测试,从而迅速地识别那些会影响到多个团队成员的错误。当某个测试失败时,修复这个失败将是团队中优先级最高的任务。游戏开发人员可以采用这个过程来确保在代码库中的所有代码都能正确工作。

2.独立的测试程序

要进行单元测试,最简单的方法就是使用一个独立的可执行程序以批处理的方式来调用所有的测试。程序员只需编写一个主模块来载入测试框架,然后确定需要运行哪些测试并且执行它们。为了增强灵活性,可以开发一个工具,每当代码中有新的测试加入,这个工具会自动把它们加到测试程序中。还可以加入生成报告的功能,譬如说把结果用电子邮件发出或是生成一个网页。

由于这个方法实现起来很方便,游戏开发人员应该尽可能地使用它。这个方法最适合用来测试那些对外部系统和资源依赖很小,并且对初始化要求不高的代码单元。这类代码单元包括类库和中间件工具、底层API、输入输出程序、数据管理工具以及游戏中其他自包含的基本元素。

3.集成的运行时测试程序

有时候,游戏测试人员想要测试的代码必须在特定的运行环境下才能执行。这往往出现在那些需要进行复杂初始化的系统中,譬如说需要从数据库载入数据并且通过网络和其他进程交互的游戏服务器。理论上说,可以把这个初始化看作单元测试数据集的一部分,每次运行测试时都可以调用这个运行时系统的一个新实例。不过,这种方法对资源和运行时间的要求使得它并不是很实用。

可以使用一个相反的模式:在运行环境中执行测试。这样,单元测试就可以对宿主进程提供的资源进行访问。使用这个方法时,游戏测试人员必须注意不要让对测试框架的调用破坏了产品代码。测试必须从宿主进程中某个位置开始,以批处理的方式执行。这个位置通常紧跟在所有必要的初始化完成之后。理想情况下,这个调用点应该只有一行代码,用来调用一个函数以启动测试过程。所有像测试识别、初始化、执行和报告之类的实际工作都应该在这个函数的代码中完成。这样就可以通过条件编译选项在开发人员编译发行版本时把这个函数调用去掉。

这种类型的测试环境最适合用于那些由很多小型系统组成的大型系统。这类代码单元包括主要的游戏系统如AI、高层的移动控制、入侵检测系统、地图或关卡初始化系统、对象管理器、消息分发系统、图形和碰撞管理系统以及其他类似子系统。

4.异步测试

测试异步代码是为MMP游戏编写单元测试的一个主要挑战。游戏开发人员所调用的代码会把控制权立即交回给调用程序(即单元测试),而在另一个线程、另一个进程或是当前线程主循环的下一次运行时再执行主要功能。大多数基于批处理的测试方式都是同步的,它们期望被测代码阻塞执行直到完成。在异步环境中按照这个假设来进行测试很可能会导致测试代码在被测代码还没开始运行时就认为它已经完成了。

测试这种代码时,工作人员需要更加小心地设计单元测试。关键就是要在被测代码执行的同时强制单元测试等待它完成。根据异步行为实现的方式,测试完成的方式也有很多种。如果这个异步行为是由一个像网络套接字这样的非阻塞I/O调用产生的,就可以使用标准的操作系统调用,像Linux上的select()或是Windows操作系统上的I/O完成端口来等待一个确认或回复。如果功能被分发到一个独立的线程中去,就可以使用操作系统的线程管理机制来等待特定的事件,然后再检查测试结果。

最后,如果异步行为是通过调度一个方法调用,使它在当前线程下一次循环时被执行所产生的,程序员就可以编写一个回调函数并且让它在被测代码完成后设置一个可以检测到的条件。这种情况主要发生于游戏开发人员是在游戏运行环境中而不是在一个独立的程序中运行测试时。此时,游戏开发人员还必须确保当前线程可以在等待结果时持续运行。为实现这点,可以把主循环封装在一个函数中,并且在测试代码内部调用它。测试自身也是运行在主循环中的,这意味着每次对这个函数的调用会把主循环引入更深一层的递归。当主循环中调用的所有代码都可以被安全地重入时,这么做通常不会产生什么问题。下面的Python例子展示了这个概念的意义。

import unittest

import gamethread # main game thread module

import dbclient   # database interface

import player, weapon   # persistent objects to test

# object persistence test

class PersistenceTest(unittest.TestCase):

    suite = unittest.TestSuite()

    def setUp(self):

        db = dbclient.InitDB()

        self.conn = db.OpenConn() # open a DB connection

        self.dbFinished = 0

    def dbCallback(self):

        # called when db processing is done

        self.dbFinished = 1

    def testSaveAndLoad(self):

        p = player.Player()

        p.name = ‘Test Player’

        p.health = 100

        p.strength = 50

        p.weapon = weapon.LongSword()

        # test save; provide callback

        self.conn.Save(p, self.dbCallback)

        # allow game loop to continue

        while not self.dbFinished:

            gamethread.Iterate()

        # done, now load

        p2 = player.Player()

        self.conn.Load(p2, dbCallback)

        self.dbFinished = 0 # reset

        # allow game loop to continue

        while not self.dbFinished:

            gamethread.Iterate()

        # now check result

        self.assert_(p2.name == ‘Test Player’)

        self.assert_(p2.health == 100)

        self.assert_(p2.strength = 50)

        self.assert_(isinstance(p2.weapon, weapon.Sword))

这个方法的主要功能都是在游戏线程(gamethread)模块中实现的,它暴露了Iterate()(枚举)函数。通过在一个while循环中调用这个函数,游戏的其他部分都可以正常运作,直到回调函数被调用并且设置标志来打断这个循环。这是一个强有力的概念,它可以测试一些本来无法测试的地方。

2.4.6  总结

单元测试是一个很有价值的工具,它可以确保那些不断发展的代码库(譬如说MMP项目)的完整性。游戏开发人员可以利用现有的框架和工具来提供标准的测试功能,从而把注意力集中在提高测试质量上。xUnit是最常见的框架,它受到了广泛的支持。工作人员也可以根据测试需要对它进行修改。从极限编程的原理出发,可以使用测试先行的设计、重构和自动化测试过程来确保代码的持续完整。游戏开发人员必须修改测试环境和工具以使它们适合所要测试的代码。这样的修改使得他们不仅可以在独立测试程序中进行测试,还可以在运行环境内部进行测试,并且在必要的时候进行异步执行。

2.4.7  参考文献

[Beck94] Beck,Kent,“Simple Smalltalk Testing: With Patterns,” http://www.xprogramming.
com /testfram.htm
,1994.

[Fowler99] Fowler,Martin,Refactoring: Improving the Design of Existing Code,Addison-Wesley,1999.

[Jeffries01] Jeffries,Ron,“What is Extreme Programming?”,http://www.xprogramming.com/ xpmag/whatisxp.htm,November 2001.

[Langr01] Langr,Jeff,“Evolution of Test and Code Via Test-First Design,”  http://www.objectmentor.
com/resources/articles/tfd.pdf
,March 2001.

[PyUnit02] van Rossum,Guido,“unittest --Unit Testing Framework,” Python Library Referencehttp://www.python.org/doc/current/lib/module-unittest.html,April 2002.

[XP99] Wells,Don,“The Rules and Practices of Extreme Programming,” http://www.
extremeprogramming.org/rules.html
,1999.

[xUnit02] xUnit Testing Frameworks, http://www.xprogramming.com/ software.htm.

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论