在本书即将带来的工程项目进一步深入助手类的细节之前,本节简单讨论一下单元测试。在上一章您已经学习了静态单元测试(static unit test)。静态单元测试非常适用于快速检测可视的结果、测试物理特性和控制器,以及快速构建游戏。但是助手类和组件不需要用户输入,而需要你考虑它们的接口。这是没有意义的,因为单元测试主要是为了改善你的应用程序的可维护性,并确保一切运行尽可能无错。例如,可以调用下面的代码行来测试Log类是否工作:
FileHelper.DeleteFile(Log.LogFilename); Log.Write("New log entry");
提示:
这段代码只能在Log类内部执行,因为Log.LogFilename是私有的。
现在,您可以进入应用程序的文件夹中查看是否有日志文件存在,并且有一个“New log entry”字样的条目在其中。不过,自己一再地检查这个文件内容是有点儿麻烦的。取代于记录下这里的每一条错误信息,你应该只把不重要的警告信息(比如,用户没有连接到Internet)放置到日志,当致命的的错误发生要抛出异常(比如,找不到纹理、shader不可用等等)。当问题变得越来越多,测试变得更加复杂并且卷入漫长的检测过程时,这么做尤其正确。通过让单元测试自我检测,你就避免了亲自检查错误,并且让它们自动地被执行,而不是像使用静态单元测试那样要你亲自从Program类中调用。
要做到上述这些,您可以使用流行的NUnit Framework,你可以在http://www.nunit.org/下载它。
另一个选择,也可以使用来自于http://www.testdriven.net/的TestDrive.Net,假设你使用的是Visual Studio 2005 Professional或更高版本。它支持很多非常酷的特性,你可以直接使用热键或者弹出菜单开始测试,这真是又酷又简单。TestDriven.Net不能工作在VC# Express或者XNA Studio Express(一年前倒是能工作的,但是开发者不得不删除这个支持Express的插件,这是因为Microsoft希望开发者在大型程序使用Professional Edition)。关于如何让XNA在Visual Studio 2005中工作,参见第一章;可以使用在XNA Studio Express中的一个虚拟项目来处理内容素材,这是在Visual Studio 2005中不可能实现的。
无论你安装哪个版本,只要从安装文件夹把NUnit.Framework.dll添加到你的工程项目中(右击你的项目,添加一个新引用;如果你在全局程序集缓存——Global Assembly Cache中找不到NUnit.Framework.dll,可以使用“浏览”,它是第一个被呈现的标签页。)。现在您可以添加下面的using指令:
#if DEBUG using NUnit.Framework; #endif
我通常在using指令区域的顶部添加这段代码,它之所以只在debug模式下使用,是因为实际上你只在debug模式下编译才使用单元测试;对于最终的游戏您也不想要这个额外的NUnit.Framework.dll以及所有的测试代码,因为你的游戏不需要它。
作为一个例子,看一看StringHelper类中使用的第一个单元测试,这个类检测IsInList辅助方法是否按预期工作:
[TestFixture] public class StringHelperTests { /// <summary> /// Test IsInList /// </summary> [Test] public void TestIsInList() { Assert.IsTrue(IsInList("whats", new string[] { "hi", "whats", "up?" }, false)); Assert.IsFalse(IsInList("no way", new string[] { "omg", "no no", "there is no way!" }, false)); } // TestIsInList() ...
Assert是NUnit框架中的一个辅助类,它包含一些检查返回值是否符合预期的方法。如果返回值不符合预期,一个异常将被抛出,您可以立即看到你的测试失败在哪一行。例如,Assert.IsTrue检查IsInList方法的返回值是否是true。如果返回值是false,一个异常将被抛出。幸运的是字符串数组中包含了“whats”,这个测试应该通过。下一个测试检查“no way”,而该字符串不在第二个字符串数组中,因此第二个测试行应该依照法则返回false。注意:“there is no way!”包含了“no way”,但此处你测试的不是Contains方法,虽然StringHelper类中也存在这个方法。如果确切的字符串在列表中被找到,IsInList方法才返回true。
通过点击鼠标右键并选择“Run Test”(如图3-6),您可以在TestDriven.Net中运行测试;如果您没有或者无法使用TestDriven.Net,也可以使用NUnit程序。您也可以借助TestDriven.Net使用相同的方式来进行静态单元测试,但NUnit GUI不支持静态单元测试。所以,我在Program.cs(或者在后面项目中的UnitTesting.cs类)中添加单元测试,来支持所有用户和XNA Studio Express。TestDriven.Net可以被用来起动动态和静态单元测试,但是自version 2.0之后您为了正常工作,必须把[Test]特性从静态单元测试中删除(总之,本书中的静态单元测试都不能使用[Test]特性)。
测试将会没有任何错误地运行,但如果你更改测试,把“whats”改成“whats up”,第一个Assert测试就会失败,并且你会看到来自于TestDriven.Net的下列结果:
TestCase 'M:XnaBreakout.Helpers.StringHelper.StringHelperTests.TestIsInList' failed: NUnit.Framework.AssertionException at NUnit.Framework.Assert.DoAssert(IAsserter asserter) at NUnit.Framework.Assert.IsTrue(Boolean condition, String message, Object[] args) at NUnit.Framework.Assert.IsTrue(Boolean condition) C:\code\XnaRacer\Helpers\StringHelper.cs(1387,0): at XnaBreakout.Helpers.StringHelper.StringHelperTests.TestIsInList() 0 passed, 1 failed, 0 skipped, took 0,48 seconds.
它会告诉您要看的确切位置(你甚至可以双击错误信息,跳转到1387行)以及应该修改什么。如果使用NUnit,错误甚至会更直观(如图3-7):
NUnit GUI是个一次性运行多个单元测试的好工具,并且很快看出哪一个不正常工作,然后深入源代码研究。你可以使用菜单“File→Load”来选择你的程序,或者把任何.Net的.exe或.dll文件拖放到NUnit GUI 程序上。然后就可以看到assembly 里面的所有单元测试,并且点击“Run”就可以测试了。程序是很好,但我通常在编码或者测试的时候不想切换出开发环境,所以我更喜欢TestDriven.Net并一直使用它。要修复这个错误,您只需把“whats up”这行改回“whats”,这样所有的测试将会通过了,你也获得了绿灯。
这里我不准备太深入单元测试,因为第二章已经讨论过了基本规则,它们也适用于动态单元测试。当开始编写你的第一个单元测试的时候,要保持这些方针:
思考您的问题,并且把它们分解成容易管理的小部分。
先写测试,不要考虑具体实现,您认为最终的代码应该是什么样,或者你想让游戏代码像什么样,就怎么写。
试着确保测试尽可能地多。例如,TestIsInList方法不仅测试了成功调用了IsInList方法,还测试了对它的失败调用。单元测试是要花些时间,但别超过50%——用不着为只有两行代码的方法编写30次的测试。
从此刻开始测试要不间断地进行,即使您认为它没有多大意义。这会迫使你看到该做什么,还有离实现过程还有多远。开始的时候,测试甚至不能编译,因为您还没有实现任何东西。当执行了一些空方法之后,测试会失败,因为你还没有做任何事情。最后当所有东西都能正常工作了,您就会感觉好多了。
即使您不非常频繁的进行静态单元测试,每一次您编译代码的时候动态单元测试都被执行(如果它们都运行得足够快)。始终设法运行所有单元测试每天一次或者每周一次,以确保你最新的代码修改不会添加新的错误。