1. Unity3D 单元测试框架介绍

Unity3D 简要介绍

如果是游戏行业的同学估计就没有不知道 Unity3D 的,腾讯的王者荣耀就是基于 Unity3D(简称:U3D)来开发的。在 U3D 中,有三个基本的概念:游戏场景(Scene)、游戏物体(GameObject)和组件(Component)。我们可以借用电影来理解这三个概念,整部电影从开始到结束,由很多场景组成,比如动作电影中的打斗场景在废旧的工厂中进行,废旧的工厂就是游戏场景;废旧工厂中的打斗场景中又由很多的物体组成,比如人、荒废的车床、被吓飞的白鸽等,这些就是游戏物体;废弃的车床又由轮子、各个钢铁部件组成,这里的轮子和部件就是游戏物体的组件。换成 U3D 中的术语就如下图所示:

以上就是 U3D 的简要介绍,如果对 U3D 有兴趣但自己又没有基础的,可以去 SiKi学院 上找免费的入门视频来学习,想要看书的话可以去看看《Unity 5.X 从入门到精通》。

Unity3D 开发环境简要说明

U3D 的开发工具叫 Unity 编辑器,在 下载地址 里选择对应的版本和平台下的 Unity 编辑器即可,比如我这里选择 Windows 平台的最新版本,如截图所示:

U3D 目前只推荐使用 C# 作为脚本开发语言,为了方便编写和调试代码通常不直接使用 Unity 编辑器作为 IDE,我们使用 Microsoft Visual Studio(简称:VS)作为 C# 脚本的开发工具,可以点击 下载链接 下载 VS,具体的安装不再详述。

安装完成 VS 后,打开 Unity 编辑器,在菜单栏上选择 Edit > Preferences…,在弹出的 Unity Preferences 对话框中选择 External Tools > External Script Editor,最后选择 VS 即可,如下图所示:

到此为止开发环境配置完成。

基于 Unity 编辑器的测试框架介绍

NUnit.Framework 介绍

在 Unity 编辑器中已经集成了单元测试框架 NUnit,关于 NUnit 可以 点击链接 了解更多,下面基于一个例子对它进行基本的介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using UnityEngine; //基于 Unity 引擎,必须引用
using NUnit.Framework; //引用NUnit测试框架

[TestFixture, Description("测试套")] //一个类对应一个测试套,通常一个测试特性对应一个测试套。
public class UnitTestDemoTest
{
[OneTimeSetUp] //在执行该测试套时首先会执行该函数,在整个测试套中只执行一次。
public void OneTimeSetUp()
{
Debug.Log("OneTimeSetUp");
}

[OneTimeTearDown] //在执行该测试套时最后会执行该函数,在整个测试套中只执行一次。
public void OneTimeTearDown()
{
Debug.Log("OneTimeTearDown");
}

[SetUp]
public void SetUp() //在执行每个用例之前都会执行一次该函数
{
Debug.Log("SetUp");
}

[TearDown] //在执行完每个用例之后都会执行一次该函数
public void TearDown()
{
Debug.Log("TearDown");
}

[TestCase, Description("测试用例1")] //这个函数内部写测试用例
public void TestCase1()
{
Debug.Log("TestCase1");
}

[TestCase, Description("测试用例2")] //这个函数内部写测试用例
public void TestCase2()
{
Debug.Log("TestCase2");
}
}

以上是NUnit的一个例子,我们在 Unity 编辑器上执行看下效果。

可以看到,执行的情况就如代码注释里的说明一样。通常,对于测试用例执行需要的必备条件的代码可以写在 OneTimeSetUp 里面,比如启动测试环境;对于测试用例执行完最后需要的清理工作可以写在 OneTimeTearDown 里面,比如退出测试环境;每个测试用例都需要初始化的公共代码写在 SetUp 里面;跑完每个测试用例都需要清理的公共代码写在 TearDown 里面。

Unity3D 单元测试的两种模式

打开 Unity 编辑器,在菜单栏依次选择 Window > Test Runner,在弹出的对话中可以看到 PlayMode 和 EditMode,这里的 Test Runner 对话框就是执行单元测试的 UI 界面,如果想进一步了解可以点击 Test Runner 官网介绍 进行深入了解。又或者在 Project 视图下依次执行 按下鼠标右键 > Create > Testing 也可以看到有 PlayMode 和 EditMode 字眼,下面是关于它两的截图。

EditMode 测试对于 Unity 编辑器而言,就是指在编辑状态下去测试,而 PlayMode 测试对于 Unity 编辑器而言,就是指在 Unity 运行时的测试。我们可以这么理解,EditMode 是代码的静态测试,测试时不需要被测代码跑起来,其实这里的 EditMode 就是跟其他编程语言的单元测试是一个意思;相对来说,PlayMode 就是代码的动态测试,被测代码需要跑起来,这时的代码环境跟业务场景结合起来。

EditMode 测试模式

上文提到 EditMode 就是传统意义上的单元测试,这里结合个例子介绍下。下面的代码是被测代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//被测代码
public class BuildManager : MonoBehaviour {
public Text moneyText;
public int money = 1500;
......

//被测试函数
public void ChangeMoney(int change = 0)
{
money += change;
moneyText.text = "¥" + money;
}
......
}

在编写 EditMode 模式的测试代码时有一点需要注意下,测试代码需要放在以 Editor 命名的文件夹下(子文件下也行,反正得在 Editor 下)才行,不然 Unity 编辑器无法识别。用例应该放在如下图所示的地方:

然后编写测试 BuildManager 对象的用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using UnityEngine;
using NUnit.Framework;
using UnityEditor.SceneManagement; //加载场景,实例化被测脚本时需要

[TestFixture]
public class BuildManagerTest : BuildManager
{
private BuildManager buildManager;

[OneTimeSetUp] //打开场景,目的是获取被测对象 BuildManager 实例
public void OneTimeSetUp()
{
EditorSceneManager.OpenScene("Assets/Scenes/MainScene.unity");
buildManager = GameObject.Find("GameManager").GetComponent<BuildManager>();
}

[OneTimeTearDown] //测试完成后销毁测试对象
public void OneTimeTearDown()
{
buildManager = null;
}

[SetUp] //初始化被测对象的属性值
public void SetUp()
{
buildManager.money = 1500;
}

[TearDown] //恢复被测对象的属性值
public void TearDown()
{
buildManager.money = 1500;
}

[TestCase] //测试被测对象的函数逻辑
public void AddMoneyTest()
{;
buildManager.ChangeMoney(100);
Assert.AreEqual(1600, buildManager.money);
}
[TestCase] //测试被测对象的函数逻辑
public void SubMoneyTest()
{
buildManager.ChangeMoney(-1000);
Assert.AreEqual(500, buildManager.money);
}
}

打开 Test Runner 对话框,选中测试用例 AddMoneyTest() 和 SubMoneyTest(),然后点击 Run Selected 即可。如下图所示:

PlayMode 测试模式

如果之前没有创建过 PlayMode 模式下的测试用例,那么打开 Test Runner 对话框,并切换到 PlayMode 页签下,你会看到如下图所示的提示:

然后点击 Enable playmode tests 按钮,再点击 Enable 确定按钮。

继续点击 OK 按钮,接着点击 Create Playmode Test with methods 按钮,发现创建了一测试脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using UnityEngine;
using UnityEngine.TestTools;
using NUnit.Framework;
using System.Collections;

public class NewPlayModeTest {

[Test]
public void NewPlayModeTestSimplePasses() {
// Use the Assert class to test conditions.
}

// A UnityTest behaves like a coroutine in PlayMode
// and allows you to yield null to skip a frame in EditMode
[UnityTest]
public IEnumerator NewPlayModeTestWithEnumeratorPasses() {
// Use the Assert class to test conditions.
// yield to skip a frame
yield return null;
}
}

这里重点介绍下 PlayMode 模式下测试用例的用法,如上代码所示,其实 [test] 的注解就是普通的测试标签,[UnityTest]标签才是 PlayMode 测试用例的标签,同时该注解下的函数返回类型是个迭代器 IEnumerator,我们注意到该函数内部还有一条语句 yield return null。其实还有类似的写法,如 yield return new WaitForSeconds()、 yield return new WaitForEndOfFrame()、 yield return new WaitForFixedUpdate() 等。如果学过 Python 我们知道在函数内部中多了 yield 语句它就是生成器,生成器不会一下子返回可迭代对象的所有数据,而是每次返回一条数据,直至迭代完成数据。这里的 yield return 语句有点类似的意思。我们之前说过,PlayMode 测试模式是在代码运行中去测试的,在 U3D 中运行的场景、物体或组件(代码脚本也是一种组件)是每一帧每一帧持续去刷新的,可以把每一帧理解成一张图片,随着时间每一帧每一帧的刷新就形成了视频动画的效果。说回这里的 yield return的作用,它可以等待运行中的场景物体刷新一段时间(可以是等一帧、等一秒等)后再继续执行下面的测试代码。这样的好处就是可以随着时间(帧不断刷新)来操作不同时期的场景、获取不同时期的场景物体信息等,来实现模拟用户或模拟场景的测试。

下面结合Demo来具体介绍下,先说说被系统:敌人(球)会在指定的路线下跑,跑到终点敌人就赢。玩家可以在指定的位置放置炮台,只要敌人走到攻击范围内,炮台就射击。现在用 PlayMode 模式简单测试下炮台的子弹是否会移动。下面是测试的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using UnityEngine;
using UnityEngine.TestTools;
using NUnit.Framework;
using System.Collections;

public class playmodetest
{
Waypoints waypoints;
Bullet bullet;
Enemy target;
Vector3 initPosition;
[SetUp] //初始化测试环境,然后获取子弹的初始位置
public void SetUp()
{
GameObject waypointsGo = new GameObject("Waypoints");
waypoints = waypointsGo.AddComponent<Waypoints>();
GameObject bulletGo = new GameObject("bullet");
bullet = bulletGo.AddComponent<Bullet>();
initPosition = bullet.transform.position;

GameObject Enemy1 = Resources.Load<GameObject>("Prefab/Enemy1/Enemy1");
Enemy enemy = Enemy1.GetComponent<Enemy>();
target = GameObject.Instantiate(enemy, enemy.transform.position, enemy.transform.rotation);
bullet.SetTarget(target.transform);
}


[UnityTest] //等待下一帧,然后比较子弹的当前位置是否与初始位置一致
public IEnumerator CheckBullet()
{
yield return null;
Assert.AreNotEqual(initPosition, bullet.transform.position);
}
}

测试结果显示测试通过:

好了,U3D的单元测试框架就先介绍到这里,后面结合实际的项目再做介绍。