
在游戏开发中,手动测试很快就会变得重复且容易出错。您是否曾在开发新功能或尝试修复错误时发现自己陷入这些看似无尽的测试周期?
通过自动化代码测试,您可以花更多时间在创意游戏开发上,而不是在重复(但重要的)质量保证任务上,这些任务确保添加、删除或更改代码不会破坏您的项目。
Unity帮助您创建、管理和运行自动化测试,使用Unity测试框架为您的游戏。
Unity测试框架(UTF)允许您在编辑和播放模式下测试您的项目代码。您还可以针对各种平台(如独立版、iOS或安卓)进行测试代码。
通过使用包管理器将UTF添加到您的项目中来安装它。
在底层,UTF与NUnit集成,NUnit是一个著名的开源.NET语言测试库。
您可以使用UTF编写的测试主要分为两类:编辑模式和播放模式:
编辑模式测试在Unity编辑器中运行,并可以访问编辑器和游戏代码。这意味着您可以测试自定义编辑器扩展,或使用测试来修改编辑器中的设置并进入播放模式,这对于调整检查器值然后使用多种不同设置运行自动化测试非常有用。
播放模式测试让您在运行时测试游戏代码。测试通常作为协程运行,使用[UnityTest]属性。这使您能够测试可以跨多个帧运行的代码。默认情况下,播放模式测试将在编辑器中运行,但您也可以在独立播放器构建中为各种目标平台运行它们。

要跟随此示例,您需要从Unity Asset Store安装Starter Assets – Third Person Character Controller package并将其导入到新项目中。

通过Window > Package Manager安装UTF。在包管理器的Unity注册表下搜索测试框架。确保选择版本1.3.3(截至撰写时的最新版本)。
安装UTF后,使用文本编辑器打开Packages/manifest.json文件,并在dependencies后添加testables部分,如下所示:
,
"testables": [
"com.unity.inputsystem"
]
保存文件。这在稍后会很有用,当您需要引用Unity.InputSystem.TestFramework程序集以进行测试和模拟玩家输入时。
返回编辑器并允许安装新版本。

单击Window > General > Test Runner以查看Test Runner编辑器窗口。
在本教程的这一部分,重点将放在创建播放模式测试上。您将使用项目窗口创建它们,而不是使用测试运行器窗口中的创建测试程序集文件夹选项。
在突出显示的项目资产文件夹根目录上,右键单击并选择Create > Testing > Tests Assembly Folder。
添加了一个Tests项目文件夹,其中包含一个Tests.asmdef(程序集定义)文件。这是测试引用您的游戏模块和依赖项所必需的。
角色控制器代码将在测试中被引用,并且还需要一个程序集定义。接下来,您将设置一些程序集定义和引用,以便在模块之间进行测试。
右键单击 Assets/StarterAssets/InputSystem 项目文件夹,然后选择 Create > Assembly Definition。给它起个描述性的名字,例如 StarterAssetsInputSystem。
选择新的 StarterAssetsInputSystem.asmdef 文件,并使用检查器添加对 Unity.InputSystem 的程序集定义引用。点击应用。
右键单击 Assets/StarterAssets/ThirdPersonController/Scripts 项目文件夹,然后选择 Create > Assembly Definition。给它起个描述性的名字,例如 ThirdPersonControllerMain。
像之前的程序集定义一样,在检查器中打开 ThirdPersonControllerMain 并选择以下引用:
- Unity.InputSystem
- StarterAssetsInputSystem
点击 Apply。

为了模拟输入系统的部分功能,您需要在测试中引用它。此外,您还需要在为第三人称控制器代码创建的程序集中的引用 StarterAssets 命名空间。
在检查器中打开 Tests.asmdef 并添加对以下程序集定义的引用:
- UnityEngine.TestRunner
- UnityEditor.TestRunner
- Unity.InputSystem
- Unity.InputSystem.TestFramework
- ThirdPersonControllerMain
点击应用。

您的第一个测试将涵盖一些关于从第三人称控制器包加载和移动主角的基础知识。
首先通过设置一个简单的测试环境场景和一个角色预制资源来创建新项目。
打开名为Assets/StarterAssets/ThirdPersonController/Scenes/Playground.unity的场景,并使用文件 > 另存为菜单将其保存为此新路径:Assets/Scenes/SimpleTesting.unity
如果您在游戏视图中注意到粉色材质,请使用渲染管线转换器将材质从内置渲染管线升级到通用渲染管线(URP)。请参见这篇文章以快速概述。
在您的项目资产文件夹中创建一个名为Resources的新文件夹。注意:文件夹名称“Resources”在这里很重要,以便可以使用Unity的Resources.Load()方法。
将场景视图中的PlayerArmature游戏对象拖放到新的Resources文件夹中,并在提示时选择创建原始预制件。将预制件资产重命名为Character.
这将是您今后测试中使用的基础角色预制件。
从新的 简单测试 场景中移除 PlayerArmature 游戏对象,并保存对场景的更改。
在初始测试设置的最后一步,转到 文件 > 构建设置,选择添加打开的场景以将 场景/简单测试 场景添加到构建设置中。
在项目资产文件夹中选择测试文件夹。右键单击并选择 创建 > 测试 > C# 测试脚本。
将新脚本命名为 角色测试。在您的 IDE 中打开脚本以更仔细地查看。
初始类文件提供了两个方法存根,演示了一些测试基础知识。
接下来,您将确保测试加载一个“以测试为中心”的游戏场景。这应该是一个包含测试您所关注的系统或组件所需的最低限度的场景。
更新 CharacterTests 类以添加两个新的 using 语句,并实现 InputTestFixture 类:
using UnityEngine.InputSystem;
using UnityEngine.SceneManagement;
public class CharacterTests :InputTestFixture
在 CharacterTests 类的顶部添加两个私有字段:
GameObject character = Resources.Load("Character");
键盘 keyboard;
角色字段将存储对从资源文件夹加载的角色预制件的引用。键盘 将持有对 InputSystem 提供的键盘输入设备的引用。
通过在 CharacterTests 类中提供您自己的方法来重写基础 InputTestFixture 类的 Setup() 方法:
公共覆盖 void Setup()
{
SceneManager.LoadScene("Scenes/SimpleTesting");
base.Setup();
keyboard = InputSystem.AddDevice();
var mouse = InputSystem.AddDevice();
按下(mouse.rightButton);
Release(mouse.rightButton);;
}
Setup() 方法运行基类的 Setup() 方法,然后通过加载测试场景和初始化键盘输入设备来设置您自己的 CharacterTests 类。
鼠标输入的添加纯粹是为了让第三人称控制器开始接收来自模拟/虚拟键盘设备的输入。这几乎就像一个‘设置焦点’的动作。
对于您的第一个测试,您将从 Prefab 实例化角色并断言它不为 null。将以下方法添加到您的测试类中:
[测试]
公共 void TestPlayerInstantiation()
{
GameObject characterInstance = GameObject.Instantiate(character, Vector3.zero, Quaternion.identity);
Assert.That(characterInstance, !Is.Null);
}
在您那里,您可能想要清理示例模板测试方法。删除 CharacterTestsSimplePasses 和 CharacterTestsWithEnumeratorPasses 方法。

保存脚本并返回到编辑器中的 测试运行器 窗口。突出显示 TestPlayerInstantiation 测试并单击 运行选定。
绿色勾号表示测试通过。您已断言该角色可以从资源中加载,实例化到测试场景中,并且在那时不为null。
您可能已经注意到,此测试使用了[Test]注释,而不是[UnityTest]注释。UnityTest属性允许协程在多个帧上运行测试。在这种情况下,您只需实例化角色并断言它已加载。
通常,您应该在编辑模式下使用NUnit测试属性,而不是UnityTest属性,除非您需要返回特殊指令、需要跳过一帧或在播放模式下等待一定时间。

接下来,您将使用UnityTest来断言按住前进控制器键会使角色向前移动。
将下面提供的新测试方法添加到您的CharacterTests类中。
出现了两个新的测试辅助方法;Press()和Release()。这两个方法都是由InputTestFixture基类提供的,帮助您模拟InputSystem控制的按下和释放。
TestPlayerMoves()方法执行以下操作:
从角色预制件实例化角色的一个实例,位置为(X:0,Y:0,Z:0)
在虚拟键盘上按下向上箭头键1秒钟,然后释放它
再等待1秒(让角色减速并停止移动)
断言角色已移动到Z轴上大于1.5个单位的位置。
保存文件,返回到测试运行器,并运行新测试。

接下来,您将通过添加一个简单的玩家健康组件来测试自定义Monobehaviour脚本。
在 Assets/StarterAssets/ThirdPersonController/Scripts 下创建一个新脚本。将其命名为 PlayerHealth。
在您的 IDE 中打开脚本,并用下面提供的代码替换内容。
这里添加了很多新代码。简而言之,这个脚本将确定玩家角色是否处于下落状态。如果在下落状态下碰到地面一次,则角色的生命值减少 10%。
在 Assets/Resources 下找到角色预制件。打开预制件并添加新的 PlayerHealth 脚本组件。
接下来,您将使用测试场景来验证玩家在从悬崖掉落后生命值是否下降。
使用 [UnityTest] 属性,您可以编写一个播放模式测试来测试掉落伤害。在下落超过 0.2 秒时,玩家应受到 0.1f 的伤害(相当于最大生命值的 10%)。
在 SimpleTesting 场景中,您将看到一段通往悬崖的楼梯。这是一个测试平台,用于在其上生成角色并测试 PlayerHealth 脚本。
再次打开 CharacterTests.cs 并添加一个名为 TestPlayerFallDamage 的新测试方法:
[UnityTest]
public IEnumerator TestPlayerFallDamage()
{
// 在测试场景中生成角色于足够高的区域
GameObject characterInstance = GameObject.Instantiate(character, new Vector3(0f, 4f, 17.2f), Quaternion.identity);
// 获取 PlayerHealth 组件的引用,并断言当前生命值为满(1f)
var characterHealth = characterInstance.GetComponent();
Assert.That(characterHealth.Health, Is.EqualTo(1f));
// 从悬崖走下并等待下落
Press(keyboard.upArrowKey);
yield return new WaitForSeconds(0.5f);
Release(keyboard.upArrowKey);
yield return new WaitForSeconds(2f);
// 断言由于跌落伤害损失了1点生命值
Assert.That(characterHealth.Health, Is.EqualTo(0.9f));
}
您还需要在类文件的最顶部添加一个 using 引用到 StarterAssets 命名空间:
using StarterAssets;
上面的测试遵循典型的 安排、执行、断言 (AAA) 模式,通常在测试中找到:
单元测试方法的 Assert 部分验证被测试的方法的行为是否如预期。
接下来,您将通过添加一个简单的玩家健康组件来测试自定义Monobehaviour脚本。
在 Assets/StarterAssets/ThirdPersonController/Scripts 下创建一个新脚本。将其命名为 PlayerHealth。
在您的 IDE 中打开脚本,并用下面提供的代码替换内容。
这里添加了很多新代码。简而言之,这个脚本将确定玩家角色是否处于下落状态。如果在下落状态下碰到地面一次,则角色的生命值减少 10%。
在 Assets/Resources 下找到角色预制件。打开预制件并添加新的 PlayerHealth 脚本组件。
接下来,您将使用测试场景来验证玩家在从悬崖掉落后生命值是否下降。
使用 [UnityTest] 属性,您可以编写一个播放模式测试来测试掉落伤害。在下落超过 0.2 秒时,玩家应受到 0.1f 的伤害(相当于最大生命值的 10%)。
在 SimpleTesting 场景中,您将看到一段通往悬崖的楼梯。这是一个测试平台,用于在其上生成角色并测试 PlayerHealth 脚本。
再次打开 CharacterTests.cs 并添加一个名为 TestPlayerFallDamage 的新测试方法:
[UnityTest]
public IEnumerator TestPlayerFallDamage()
{
// 在测试场景中生成角色于足够高的区域
GameObject characterInstance = GameObject.Instantiate(character, new Vector3(0f, 4f, 17.2f), Quaternion.identity);
// 获取 PlayerHealth 组件的引用,并断言当前生命值为满(1f)
var characterHealth = characterInstance.GetComponent();
Assert.That(characterHealth.Health, Is.EqualTo(1f));
// 从悬崖走下并等待下落
Press(keyboard.upArrowKey);
yield return new WaitForSeconds(0.5f);
Release(keyboard.upArrowKey);
yield return new WaitForSeconds(2f);
// 断言由于跌落伤害损失了1点生命值
Assert.That(characterHealth.Health, Is.EqualTo(0.9f));
}
您还需要在类文件的最顶部添加一个 using 引用到 StarterAssets 命名空间:
using StarterAssets;
上面的测试遵循典型的 安排、执行、断言 (AAA) 模式,通常在测试中找到:
单元测试方法的 Arrange 部分初始化对象并设置传递给被测试方法的数据的值。
单元测试方法的 Act 部分使用安排的参数调用被测试的方法。在这种情况下,调用被测试的方法是通过物理交互处理的,当玩家在跌落后撞击地面时。
单元测试方法的 Assert 部分验证被测试的方法的行为是否如预期。

回到编辑器,运行新的测试。在播放模式下运行时,您会看到角色走出边缘,跌落(超过0.2秒的阈值以分类为跌落),并在撞击地面后受到伤害。
测试不仅用于确保代码更改不会破坏功能,它们还可以作为文档或指针,帮助开发人员在调整设置时考虑游戏的其他方面。

如前所述,在测试运行器中运行播放模式测试默认会在 Unity 编辑器中以播放模式运行它们。您也可以更改它们以在独立播放器下运行。
使用测试运行器窗口中的运行位置下拉选择来切换测试以在独立播放器构建中运行。
一旦您开始构建测试套件,下一步就是在构建完成后自动运行它们。在构建后运行的自动化单元和集成测试对于尽早捕捉回归或错误非常有用。它们也可以作为云中的远程自动化构建系统的一部分运行。
通常,您会希望以自定义格式捕获测试运行结果,以便结果可以与更广泛的受众共享。为了在 Unity 编辑器之外捕获测试结果,您需要将构建和运行过程分开。
在您的测试项目文件夹中创建一个名为SetupPlaymodeTestPlayer的新脚本。
SetupPlaymodeTestPlayer 类将实现 ITestPlayerBuildModifier 接口。您将使用它来覆盖并“挂钩”到 ModifyOptions 方法,该方法接收构建的播放器选项,并允许您修改它们。
using System.IO;
using UnityEditor;
using UnityEditor.TestTools;
[assembly:TestPlayerBuildModifier(typeof(SetupPlaymodeTestPlayer))]
public class SetupPlaymodeTestPlayer :ITestPlayerBuildModifier
{
public BuildPlayerOptions ModifyOptions(BuildPlayerOptions playerOptions)
{
playerOptions.options &= ~(BuildOptions.AutoRunPlayer | BuildOptions.ConnectToHost);
var buildLocation = Path.GetFullPath("TestPlayers");
var fileName = Path.GetFileName(playerOptions.locationPathName);
if (!string.IsNullOrEmpty(fileName))
buildLocation = Path.Combine(buildLocation, fileName);
playerOptions.locationPathName = buildLocation;
返回 playerOptions;
}
}
此自定义 Player Build 修改脚本在 Play 模式下运行测试时执行以下操作(运行位置:在 Player 上):
禁用已构建玩家的自动运行,并跳过尝试连接到其运行的主机的玩家选项
将构建路径位置更改为项目内的专用路径(TestPlayers)
完成此操作后,您可以期待构建在 TestPlayers 文件夹中找到,每当它们完成构建时。这现在完成了构建修改,并切断了构建与运行之间的链接。
接下来,您将实现结果报告。这将允许您将测试结果写入自定义位置,准备进行自动报告生成和发布。
在您的 Tests 项目文件夹中创建一个名为 ResultSerializer 的新脚本(如下所示)。此类将使用对 TestRunCallback 的程序集引用,并实现 ITestRunCallback 接口。
此 ITestRunCallback 的实现包括一个自定义的 RunFinished 方法,该方法设置一个带有测试的玩家构建,以将测试结果写入名为 testresults.xml 的 XML 文件。

结合 SetupPlaymodeTestPlayer.cs 和 ResultSerializer.cs,构建和运行过程现在已分开。运行测试将结果输出到位于玩家平台的 Application.persistentDataPath 位置 的 testresults.xml。
要使用这些钩子类中的某些类型,您需要添加对 Tests.asmdef 的额外引用。更新它以添加 UnityEditor.UI.EditorTests 程序集定义引用。
在 Player 中运行测试现在将在您的项目下的 TestPlayers 文件夹中生成一个玩家构建输出,并在 Application.persistentDataPath 位置生成一个 testresults.xml 文件。

Unity 测试框架课程
测试框架包包括 一个测试课程,其中包含示例练习,帮助您更深入地了解如何使用 Unity 进行测试。确保使用包管理器获取课程的项目文件。
使用 包管理器 > 包:Unity 注册表 > 测试框架,找到示例下拉列表并导入课程练习。
练习将被导入到您的项目中,并位于 Assets/Samples/Test Framework 下。每个示例都包括一个练习文件夹供您使用,以及一个解决方案供您在跟随时与自己的工作进行比较。
使用 UTF 进行代码质量检查
这场 Unite 哥本哈根演讲 讨论了 UTF 的更多细节,并提供了一些其他有趣的测试自定义用例。确保查看以了解还有什么可能。
Unity中的调试
通过以下文章加快您在 Unity 中的调试工作流程:
- Microsoft Visual Studio 2022
- Microsoft Visual Studio Code
高级技术电子书
Unity 提供了许多高级指南,帮助专业开发人员优化游戏代码。创建 C# 风格指南:编写更清晰的代码,扩展 汇集了行业专家关于如何创建代码风格指南的建议,以帮助您的团队开发干净、可读和可扩展的代码库。
我们用户中另一个受欢迎的指南是 70+ 提高 Unity 生产力的技巧。它包含了节省时间的技巧,以改善您在 Unity 2020 LTS 中的日常工作流程,包括即使是经验丰富的开发人员可能错过的技巧。
文档
进一步探索最新的 TestRunner API,了解其他 UTF 自定义属性,并发现可以与 UTF 文档 钩入的更多生命周期。
在 Unity 最佳实践中心 中找到所有 Unity 的高级电子书和文章。