3.4 XNA 中的单元测试
 

     在进一步学习辅助类之前,本节先来简单地讨论一下单元测试。上一章您已经学习了静态单元测试(static unit test),静态单元测试非常适用于快速检测输出结果、测试物理特性和控制器以及快速构建游戏。但是辅助类和组件不需要用户输入,这就要求您自己来处理它们。这是没有意义的,因为单元测试主要是为了改进应用程序的可维护性,并确保尽可能少地出现错误。例如,可以使用下面的代码来测试Log类是否可以工作:

FileHelper.DeleteFile(Log.LogFilename);
Log.Write(
"New log entry");

提示:这段代码只能在Log类内部执行,因为Log.LogFilename是私有的。

     现在,您可以查看一下应用程序的目录中是否有日志文件,并且其中包含“New log entry”信息。不过,自己反反复复地检查文件内容是很麻烦的。不要把所有错误信息都记录下来,您只需记录下不是特别重要的警告信息(比如用户没有连接到Internet),而对于严重的错误要抛出异常(比如找不到textureshader不可用等等)。当问题变得越来越多,测试变得更加复杂并且需要漫长的检测过程,尤其应当这么做。您可以用单元测试来进行错误检测,这样就不用亲自去做了,也可以让它们自动地被执行,而不是像使用静态单元测试要亲自从Program类中去调用。

NUnit和TestDriven.Net

     要达到上述目的您可以使用流行的NUnit Framework,可以在这里下载:http://www.nunit.org

     另外,如果您使用的是Visual Studio 2005 Professional或更高版本,也可以使用TestDrive.Net,可以从http://www.testdriven.net 得到。它支持很多非常好的特性,直接使用热键和弹出菜单就可以进行测试,非常简单。VC# ExpressXNA Studio Express不支持TestDriven.Net(一年前倒是支持的,但由于Microsoft希望开发者使用Professional版本,所以Express版本就不再支持这个插件)。关于在Visual Studio 2005中如何使用XNA可以参见第一章的相关内容,可以在XNA Studio Express中使用一个临时项目来处理内容素材,因为在Visual Studio 2005中不支持。

     不管安装哪个版本,只要把动态库NUnit.Framework.dll添加到项目中就可以了(右键单击项目添加一个新的引用,在打开的窗口中第一个标签页显示的是全局程序集缓存——Global Assembly Cache,如果这里找不到NUnit.Framework.dll,可以切换到“浏览”标签页定位到NUnit的安装目录中找到它)。现在您可以添加下面的指令:

#if DEBUG
using NUnit.Framework;
#endif

我通常把这段代码放在using指令区域的顶部,而且它之所以只在调试模式下使用,是因为只有在调式模式下才会使用单元测试,对于最终发布的游戏您也不想要这些额外的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()

其中AssertNUnit框架中的一个辅助类,它包含一些检查返回值是否正确的方法。如果返回值不正确,就会抛出一个异常,您可以立即看到是哪行出了错。例如上述代码中,Assert.IsTrue检查IsInList方法的返回值是否是true,如果是false,就抛出一个异常。其中第一个测试的字符串数组中包含了字符串“whats”,返回值就是true,那么这个测试就通过了。第二个测试是否存在“no way”,而字符串数组中没有,那么返回值就是false。请注意:虽然“there is no way!”中包含了“no way”,但此处测试的不是Contains方法(虽然StringHelper类中的确存在这个方法)。只有当字符串列表中完整包含一个字符串的时候,IsInList方法才返回true

开始单元测试

     您可以使用TestDriven.Net运行测试,单击鼠标右键并选择“Run Test”(如图3-6所示),如果您没有或者无法使用TestDriven.Net,也可以使用NUnit程序。您也可以在TestDriven.Net中使用相同的方式来做静态单元测试,但NUnit GUI不支持静态单元测试。所以,我在Program.cs(或者是后面项目中使用的UnitTesting.cs类)中添加单元测试来支持所有用户和XNA Studio ExpressTestDriven.Net可以用来进行动态和静态单元测试,但2.0 版本之后您必须把[Test]特性从静态单元测试中去掉,否则无法正常工作(本书中的静态单元测试都没有使用[Test]特性)。
图3-6
如图3-6

     此时运行测试不会产生任何错误,但如果把“whats”改成“whats up”,第一个Assert测试就会失败,并得到下面的结果:

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所示):
图3-7
3-7

     NUnit GUI是一个非常好的工具,一次可以运行多个单元测试,并很快看到哪些出了错,然后深入源代码追查错误。您可以使用菜单“FileLoad”来选择程序,或者直接把任何.Net开发的.exe.dll文件拖放到它上面,然后就可以看到程序集里面的所有单元测试,点击“Run”就可以测试了。它虽然很好使,但通常我在编码或者测试的时候不想切换出开发环境,所以我更喜欢TestDriven.Net并一直使用它。要修复这个错误,您只需把“whats up”改回“whats”,这样所有的测试就可以通过了。

黄金法则

     这里我不准备写太多关于单元测试的东西,因为第二章已经讨论了最基本的规则,它们也适用于动态单元测试。当您开始写第一个单元测试的时候,要紧记下面的这些规则:
  • 仔细分析您的问题,然后把它们分解成容易处理的小问题
  • 先写测试,不要考虑具体怎么实现,您认为最终的游戏代码会是什么样,就怎么写
  • 测试要尽可能地多。例如,前面的TestIsInList不仅测试了成功调用IsInList,还测试了对它的失败调用。单元测试是要花些时间,但千万别超过50%——用不着为只有两行代码的方法进行多达30次的测试
  • 从此刻开始测试要不间断地进行,即使您认为它没有多大意义。这会让您意识到该做什么,还有多少东西要实现。开始的时候,测试可能都无法通过编译,因为您还没有实现任何东西。当实现了一些空的方法之后,测试会失败,因为并没有做实际的事情。最后当所有东西都能正常工作了,您就会感觉好多了
  • 即使您不经常测试静态单元测试,但每次您编译代码的时候动态单元测试都被执行(如果它们都运行得足够快)。每天一次或者每周一次运行所有单元测试,来确保对代码做的最新修改不会导致新的错误。