使用Unity Test Runner进行测试驱动开发
测试驱动开发(Test-Driven Development ,简称TDD)是指在编写实际代码之前,针对功能代码编写动态测试的实践方法。本文将由Unity技术经理Sophia Clarke讲解如何将测试驱动开发应用于开发游戏,并说明哪些方法可行,哪些不可行。
TDD不是一个万能的解决方案,但我们可以从该方法中获益。本文我们在项目中使用了Unity Test Runner,它是用于在Unity中编写和执行NUnit测试的系统。
TDD的常用工作流程如下:
确定代码将执行的操作,假设代码会将CodeHasRun的值由False设为True。
编写测试,检查该代码是否正常运行。此时测试会检查CodeHasRun是否为True。
运行测试。因为代码还未编写,所以测试应该会失败。如果测试没有失败,那么该测试就存在问题,或是开发者对代码的理解有问题。
编写实际代码。
再次运行测试,此时测试应该能够通过。
遵循此工作流程可加快代码重构和改动的过程,因为这样做可以直接发现产生问题的位置和原因。
或许你会疑惑为什么我们需要在编写代码前编写测试?这是因为编写代码后,再编写测试往往会导致开发人员为了通过测试而编写测试。如果事先编写失败测试,就能确定它失败的合理原因,并排除误报现象。
TDD通常用于软件开发,很少用于游戏开发。而来自Unity发布工程组不同团队的五名成员开始研究使用TDD来制作游戏。
我们曾了解到,一些开发者认为他们无法使用自己的游戏代码实现自动化游戏,而且也无法使用TDD,所以我们打算亲自进行实验。
我们决定使用几款经典游戏进行测试:Pong、Snake、Asteroids和Flappy Bird。这样做的优点是,我们不需要在游戏设计上花费时间,因为我们已经大致了解如何组成游戏内容。
虽然我们知道每个游戏的过程,但仍需要深入研究其中的概念,以便了解如何构建测试。以Pong为例,我们知道球板会进行移动,但究竟什么是球板呢?所以我们要对每个概念进行分析。
我们将Pong中的球板划分为以下属性:
一个大小为(0.5, 2, 0)的矩形
可以在XY平面上移动
无法在Z轴移动
无法左右移动
无法移出屏幕边界之外
会发生碰撞
属于Kinematic Rigidbody
这样做给编写测试提供了很好的起点。例如:
[Test]
public void AtLeastOnePaddleIsSuccesfullyCreated()
{
GameObject[] paddles = CreatePaddles();
//断言该球板对象已存在
Assert.IsNotNull(paddles);
Assert.IsNotNull(paddles);
}
[Test]
public void TwoPaddlesAreSuccesfullyCreated()
{
GameObject[] paddles = CreatePaddles();
// 断言球板的数量等于2
Assert.AreEqual(2, paddles.Length);
}
从测试中,可以看见我们需要编写一个名为CreatePaddles的方法,该方法会创建一个包含二个游戏对象的数组。
Unity Test Runner 包含UnityTest等功能。UnityTest会返回IEnumerator,并在运行模式以协程的形式运行,从而允许我们测试需要一帧或多帧完成的动作。
本示例中,我们将使用UnityTest检查球板无法离开屏幕边界。
[UnityTest]
public IEnumerator Paddle1StaysInUpperCameraBounds()
{
// 提高timeScale,使游戏快速运行
Time.timeScale = 20.0f;
// _setup是TestSetup类的成员,用于保存设置测试场景的代码(从而不必大量复制代码)
Camera cam = _setup.CreateCameraForTest();
GameObject[] paddles = _setup.CreatePaddlesForTest();
float time = 0;
while (time < 5)
{
paddles[0].GetComponent<Paddle>().RenderPaddle();
paddles[0].GetComponent<Paddle>().MoveUpY("Paddle1");
time += Time.fixedDeltaTime;
yield return new WaitForFixedUpdate();
}
// 重置 timeScale
Time.timeScale = 1.0f;
// 球板边缘不应该离开屏幕边缘
// (Camera.main.orthographicSize - paddle.transform.localScale.y /2)将计算出球板边界与屏幕边界相接触的位置,0.15是为其等待下一帧设定的误差限度
Assert.LessOrEqual(paddles[0].transform.position.y, (Camera.main.orthographicSize - paddles[1].transform.localScale.y /2)+0.15f);
}
单元测试应该是测试功能的最小部分,所以我们测试了每块球板在屏幕顶部和底部的情况。
如果我们在此使用deltaTime,该测试会变得不稳定,因为结果会发生变化。我们设置了Time.captureFramerate或fixedDeltaTime,使测试结果可以预测。
如果开始时没有设置Time.timeScale,该测试会耗费5秒以上的时间完成,这对测试来说时间太长。将timeScale设为20表示测试速度会比正常速度快20倍,这意味着原本5秒的测试会在约0.25秒的时间内完成。
上述测试中,我们使球板向上移动了5秒。该测试检查球板是否受MoveUpY方法的限制而无法移出屏幕。首次编写测试时,MoveUpY没有阻止球板移出屏幕的功能,这表示此时测试会失败。
编写测试后,在编写实际代码前,一定要检查该测试在运行时失败,否则会得到误报情况。
项目早期,我们编写了一个测试,但忘记检查它是否会失败,然后该测试即使在游戏功能无法正常运行时也能通过。当回头检查测试时,我们发现编写了错误的代码。我们必须吸取这个教训,并保证以后在编写功能前检查测试是否失败。
Unity Test Runner允许开发者重新运行失败的测试,从而有助于加速迭代过程,即使项目中有多个测试。
使用TDD可能在开始时进展较慢,但一旦开始使用它,将是一个非常有益的工作方式。这意味着在项目后期,可以更快更安全地对项目进行改动。
这种工作方式也有助于塑造游戏中不同系统的运作方法。当我们重新创建已经熟悉的游戏时,我们不确定设计的各个系统该如何组成。事先编写测试能让我们安排各项功能,然后实现这些功能以了解它们是否可行。需要的话可以由此进行迭代。
使用TDD能帮助我们编写出更好更简洁的代码,因为我们需要更多地考虑编写的内容和原因。
一定要记住,TDD不是一个全能的解决方案,但它是一个不错的保障措施,当所有测试都安排好后,就能实现项目的快速迭代。看到Unity Test Runner窗口中出现一个个绿色标记,相信会让你很有满足感。
使用TDD并不意味着你不需要进行其它类型的测试,TDD是一种质量驱动的开发方法,而不是质量保证策略。
最后,希望开发者们使用TDD创作出更佳的游戏。更多Unity技术经验分享尽在Unity官方中文论坛(UnityChina.cn) !
推荐阅读
官方活动
Unity的新订阅用户在获得优惠礼包的同时,我们将邀请活动期间所有的订阅用户参与Unity的圣诞派对,共庆年终圣诞季![了解详情...]
Asset Store年终巨惠Cyber Week劲爆来袭
Asset Store资源商店年终巨惠Cyber Week劲爆来袭!多至2千余款人气热销资源参与促销,低至5折优惠,更有4.5折特惠组合资源包,好资源和低价格一包打尽!
活动地址:
https://assetstore.unity.com
点击“阅读原文”访问Unity官方中文论坛