热度 2|
当我最近开始致力于新的游戏项目开发时,我仔细思考了如何进行代码的单元测试。我知道,如果我将这一工作留到以后并紧接着使用相同的游戏代码创建其它内容,我可能不会意再回到这里花时间进行测试。
对我来说,编写单元测试需要面对两大挑战。首先也是最重要的是,游戏与其它类型的软件并不相同,它有一大部分专门处理输入,视觉效果/图像/用户界面(UI)的代码;而这两方面内容更是系统中最典型的“难以进行单元测试”的部分。
例如,你应该编写何种测试以检查你的手榴弹爆炸设置是否合理?(需要注意的是还有其它类型的测试能够适应于这种场景,或者至少不会让你的游戏逆行发展,不过它们并不属于单元测试)
而第二大挑战便是在Unity平台上进行单元测试。这便是我在本篇文章中将重点强调的内容。
Unit Tests(from planetgeek.ch)
单元测试框架
我一直在寻找适用于MonoDev和Unity的框架。并且我也发现了一些免费的解决工具,如NUnitLite,UUnit以及Sharp Unit。同时还有一些商用产品,如Test Star(具有更多功能)。
考虑到免费解决工具大多都是过时产品,并且具有一定的漏洞,同时也因为我当时的预算较为有限,我便决定自己创建合适的测试工具。我不打算在此详细说明这一工具,不过它的基本原理与NUnit差不多。
定义TestFixture以及SetUp等属性,并基于你的测试管理者的反应去寻找所有的测试类以及它们的测试方法,调用并寻找任何问题,并将其保存在一个清单列表中。最后在UI扩展中呈现这一清单中的内容。
扩展单元编辑器
在此我想对所有使用Unity的团队强调,这是一个能够帮助你轻松扩展UI的工具!我很惊讶自己竟然只执行了两种方法便快速将测试UI整合到编辑器菜单和窗口中,并且这也只是基于Unity的免费版本。
同时我也思考了其它能够用于添加的工具,包括数据编辑器。但是我遇到的一个问题是,我们不能在工作线程中运行MonoBehaviour代码,并且Repaint调用也不能帮助我们快速重新绘制UI。如此看来,我们也只能在运行了所有测试后才能更新测试结果。
所以我真正能够测试的是哪些内容?
这是个很难回答的问题。不过最根本的是,我们不可能测试所有问题。就像我之前提到的,涉及图像和视觉效果的代码并不适合进行单元测试。以下是我做出的一些结论,将帮助我们进行更深入的研究。
分离视觉元素的逻辑
具体地来说,纯粹的类并不是源于MonoBehaviour,并且也不是用于处理GameObjects以及其它特定的场景架构。就像是你的数据存取类;或者与你的AI相关联的类。
除此之外MonoBehaviour中还有更多微妙的类可助你将Update等方法中的逻辑重构成一个更易测试的Helper类。但是你也需要注 意,这并不是意味着你不能在这些类中使用任何Unity类型,但如果它们依赖于场景,其它对象,组件以及状态,那么事情就变得有点棘手了。
说到数据存取,如果你曾经考虑过为游戏创建一个数据存取或数据库,你就需要保证它能够支持内存模式(因为对于测试来说这是最理想的模式)。你可以调用“:memory:”在运行测试期间断开连接字符串,绕开所有与文件处理有关的问题并加速测试。
测试MonoBehaviours
当然了,在MonoBehaviours中也存在你难以忽视的逻辑,你需要对其进行单独测试。现在如果你在自己的脚本中简单地实例化MonoBehaviour,你将会在控制台中看到如下问题:
你正尝试使用“新的”关键词创建MonoBehaviour。但这并不是可行做法,你只能够使用AddComponent()添加MonoBehaviours。你的脚本只能源自ScriptableObject,或者不从属于任何一个基本类。
MonoBehviour只能基于其上层对象而存在。如果它并未修改任何与该对象相关的内容,它就不能成为真正的MonoBehaviour。为了做到这一点,我整合了一个简单的工具类,如下:
public class ScriptInstantiator
{
private List GameObjects { get; set; }
public ScriptInstantiator()
{
GameObjects = new List();
}
public T InstantiateScript<T>() where T : MonoBehaviour
{
GameObject gameObject;
object prefab = Resources.Load(“Prefabs/” + typeof(T).Name);
// If there is no prefab with the same name, just use an empty object
//
if (prefab == null)
{
gameObject = new GameObject();
}
else
{
gameObject = GameObject.Instantiate(Resources.Load(“Prefabs/”
+ typeof(T).Name)) as GameObject;
}
gameObject.name = typeof(T).Name + ” (Test)”;
// Prefabs should already have the component
T inst = gameObject.GetComponent<T>();
if (inst == null)
{
inst = gameObject.AddComponent<T>();
}
// Call the start method to initialize the object
//
MethodInfo startMethod = typeof(T).GetMethod(“Start”);
if (startMethod != null)
{
startMethod.Invoke(inst, null);
}
GameObjects.Add(gameObject);
return inst;
}
public void CleanUp()
{
foreach (GameObject gameObject in GameObjects)
{
// Destroy() does not work in edit mode
GameObject.DestroyImmediate(gameObject);
}
GameObjects.Clear();
}
}
InstantiateScript()方法为脚本创造了一个合适的预制对象,如果没有对应的内容它就会创建一个空白对象以及相关联的脚本类。然后 便是有效地调用Start() 方法。如果你还使用了其它方法,如Awake(),那么你也需要调用它们。在这种情况下我们需要公开Awake/Start/Update方法,以便你能 够在测试中进行调用。
这并非一桩易事,因为MonoBehaviour的初始化将会更加复杂,并且它所拥有的代码可能并不完整。但一般情况来看,这并不会有太大问题。
另外需注意的是,我将从资源文件夹中加载预制件,并且确保它们的名字始终与脚本相一致。而如果你面对的是一款较复杂的游戏,即在不同的预制件中使用相同的脚本作为一个组件,你就需要明确标注每一个预制件的名称。
除此之外,有时候你可能会只是出于测试目的而创建一个简单的预制件。这时候你就不应该将测试预制件保存在资源文件夹中(例如Assets/TestPrefabs文件夹),确保将它们不影响项目开发。
然后你需要在TearDown方法中调用CleanUp方法,以下是测试例子:
[Test]
public void MovingEntitiesUpdatesConnector()
{
var source = ScriptInstantiator.InstantiateScript<Entity>();
var target = ScriptInstantiator.InstantiateScript<Entity>();
var connector = ScriptInstantiator.InstantiateScript<Connector>();
connector.SetSourceEntity(source);
connector.SetTargetEntity(target, true);
source.transform.position = new Vector3(-10.0f, 0.0f, 0.0f);
target.transform.position = new Vector3(0.0f, 10.0f, 0.0f);
connector.Update();
Assert.IsTrue(Vector3.Distance(connector.transform.position,
source.transform.position) < 0.01f);
Assert.IsTrue(Vector3.Distance(connector.EndPoint,
target.transform.position) < 0.01f);
}
有所选择地进行测试
Richard曾提到关于游戏开发的反复试验问题。尽管大多数软件都易于进行设计更改,但我认为它们远不及游戏所具有的灵活且容易改变的功能。因此,游戏开发更容易导致我们编写出大量频繁变化的测试,这也会成为一种累赘。
当然了,这里也存在着许多硬性规则。我们需要根据经验和实践,明确哪些代码是可以测试并且不会频繁改变,以及哪些内容不够稳定等。同时我们也需要牢记,所有的代码都必须经得起改变和测试,并且不要因为害怕变动而恐于编写测试。
最后,我们还需要知道,有些单元测试在开发后期阶段执行才会更有效,这个时候的改变也不会如此频繁。例如,当你在测试阶段遇到一个漏洞,你便只需要编写一个测试代码先运行漏洞再编写一个修改代码即可。这可以让你的代码摆脱逆行的结果。
小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )
GMT+8, 2025-4-26 08:16 , Processed in 0.078999 second(s), 15 queries .
Powered by Discuz! X3.5 Licensed
© 2001-2025 Discuz! Team.