关于 unity-testing
unity-testing 是 Unity 内置测试框架的参考技能,面向 Unity 开发者。它介绍 Edit Mode 与 Play Mode 测试、测试模式,以及用于自动验证的命令行执行。
主要功能
- 01Edit Mode 与 Play Mode 测试
- 02NUnit 和 Unity 专用属性
- 03协程与异步测试模式
- 04Test Runner 与命令行执行
- 05输出 NUnit XML 测试结果
适用场景
- 01在 Edit Mode 测试纯 C# 逻辑
- 02在 Play Mode 验证游戏玩法和物理
- 03在 CI/CD 流水线运行测试
- 04导出 NUnit XML 测试结果
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nice-wolf-studio/ unity-claude-skills --skill unity-testing安装后在 Claude Code 中输入/unity-testing确认已加载。
附带文件references/test-framework.md
详细介绍
Unity Test Framework
测试框架概述
Unity Test Framework(UTF)是 Unity 内置的测试解决方案,集成了为 Unity 调整的 NUnit 自定义版本(基于 NUnit 3.5)。它支持在 Edit Mode、Play Mode 和 目标平台(Standalone、Android、iOS)上测试 C# 代码。
UTF 通过 Unity 专用特性([UnityTest]、[UnitySetUp]、[UnityTearDown])扩展 NUnit,支持协程、yield 指令和基于帧的执行。
主要功能:
- 用于编辑器扩展和纯逻辑的 Edit Mode 测试
- 用于游戏玩法、物理和基于协程的代码的 Play Mode 测试
- 用于平台专属验证的 Standalone 玩家测试构建
- 用于 CI/CD 流水线的命令行执行
- NUnit XML 结果输出
设置和配置
安装测试框架
UTF 默认随 Unity 一同提供。通过 Window > General > Test Runner 打开 Test Runner。
创建测试程序集
测试代码必须放在自己的程序集定义文件(.asmdef)中,并引用 NUnit。
可通过 Window > General > Test Runner(点击 Create a new Test Assembly Folder)或 Assets > Create > Testing > Test Assembly Folder 创建。
这会创建一个 Tests 文件夹,其中包含一个预先配置了以下引用的 .asmdef 文件:
nunit.framework.dllUnityEngine.TestRunnerUnityEditor.TestRunner(仅限 Edit Mode)
Edit Mode 程序集定义(.asmdef)
{
"name": "Tests",
"references": [
"UnityEngine.TestRunner",
"UnityEditor.TestRunner"
],
"includePlatforms": ["Editor"],
"defineConstraints": ["UNITY_INCLUDE_TESTS"],
"autoReferenced": false
}
Play Mode 程序集定义(.asmdef)
{
"name": "Tests.PlayMode",
"references": [
"UnityEngine.TestRunner",
"UnityEditor.TestRunner",
"MyGameAssembly"
],
"includePlatforms": [],
"optionalUnityReferences": ["TestAssemblies"],
"defineConstraints": ["UNITY_INCLUDE_TESTS"],
"autoReferenced": false
}
重要: 要测试游戏代码,请在 references 数组中添加对包含待测代码的程序集的引用。
来源: 创建测试程序集
Edit Mode 与 Play Mode 测试
| 方面 | Edit Mode | Play Mode |
|---|---|---|
| 执行上下文 | EditorApplication.update 回调循环 | MonoBehaviour 上的协程 |
| 编辑器访问权限 | 完整的 Editor 和游戏代码 | 仅游戏代码(除非在 Editor 中) |
| 协程支持 | yield return null 会跳过一次更新 | 完整的 yield 指令(WaitForSeconds 等) |
| 平台目标 | 仅限 Editor | Editor Play Mode 和独立玩家 |
| 适用场景 | 编辑器扩展、纯逻辑、序列化 | 物理、游戏玩法、MonoBehaviour 生命周期 |
| 程序集平台 | "includePlatforms": ["Editor"] | "includePlatforms": [] |
适合使用 Edit Mode 的场景:
- 测试编辑器工具、自定义检查器、ScriptableObject 逻辑
- 不依赖运行时系统的纯 C# 逻辑
- 需要访问
UnityEditorAPI 的测试
适合使用 Play Mode 的场景:
- 测试 MonoBehaviour 生命周期(Awake、Start、Update)
- 需要
WaitForFixedUpdate的物理交互 - 协程和异步工作流
- 运行时的场景加载和对象实例化
最佳实践: 除非需要 yield 指令,否则请使用 NUnit [Test],而不是 [UnityTest]。[Test] 运行速度更快,因为它不需要协程开销。
编写测试
通过 Assets > Create > Testing > C# Test Script 或点击 Test Runner 中的 Create a new Test Script 按钮创建测试脚本。
基本测试结构
using NUnit.Framework;
[TestFixture]
public class HealthSystemTests
{
private HealthSystem _health;
[SetUp]
public void SetUp() => _health = new HealthSystem(maxHealth: 100);
[TearDown]
public void TearDown() => _health = null;
[Test]
public void TakeDamage_ReducesHealth()
{
_health.TakeDamage(30);
Assert.AreEqual(70, _health.CurrentHealth);
}
[Test]
public void TakeDamage_CannotGoBelowZero()
{
_health.TakeDamage(150);
Assert.AreEqual(0, _health.CurrentHealth);
}
[Test]
public void Heal_IncreasesHealth()
{
_health.TakeDamage(50);
_health.Heal(20);
Assert.AreEqual(70, _health.CurrentHealth);
}
}
NUnit 特性参考
| 特性 | 用途 |
|---|---|
[Test] | 将方法标记为同步测试 |
[TestFixture] | 将类标记为包含测试 |
[SetUp] | 在每个测试方法运行前执行 |
[TearDown] | 在每个测试方法运行后执行 |
[OneTimeSetUp] | 在测试夹具中的所有测试运行前执行一次 |
[OneTimeTearDown] | 在测试夹具中的所有测试运行后执行一次 |
[TestCase(args)] | 使用内联数据的参数化测试 |
[Values(args)] | 为测试参数提供值 |
[Category("name")] | 对测试进行分类,以便筛选 |
[Ignore("reason")] | 跳过测试并说明原因 |
[Timeout(ms)] | 设置测试超时时间 |
Unity 专用特性
| 特性 | 用途 |
|---|---|
[UnityTest] | 基于协程的测试,支持 yield 指令 |
[UnitySetUp] | 基于协程的测试设置(支持 yield) |
[UnityTearDown] | 基于协程的测试清理(支持 yield) |
[RequiresPlayMode] | 强制测试在 Play Mode(true)或 Edit Mode(false)下运行 |
[UnityPlatform] | 限制测试只能在指定平台运行 |
[TestMustExpectAllLogs] | 要求所有日志消息都必须是预期的 |
[PrebuildSetup] | 在构建播放器前执行设置 |
[PostBuildCleanup] | 在构建播放器后执行清理 |
[ConditionalIgnore] | 有条件地忽略测试 |
[TestRunCallback] | 订阅测试进度更新 |
常见断言
Assert.AreEqual(expected, actual); // Equality
Assert.IsTrue(condition); // Boolean
Assert.IsNotNull(obj); // Null check
Assert.Throws<ArgumentException>(() => F()); // Exception
Assert.AreEqual(1.0f, actual, 0.01f); // Float tolerance
StringAssert.Contains("sub", actualString); // String
CollectionAssert.Contains(list, item); // Collection
完整的断言 API 参考请参阅 references/test-framework.md。
来源: 创建测试
测试 MonoBehaviour
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using System.Collections;
[TestFixture]
public class PlayerControllerTests
{
private GameObject _playerObject;
private PlayerController _player;
[UnitySetUp]
public IEnumerator SetUp()
{
_playerObject = new GameObject("Player");
_player = _playerObject.AddComponent<PlayerController>();
yield return null; // Wait one frame for Awake/Start
}
[UnityTearDown]
public IEnumerator TearDown()
{
Object.Destroy(_playerObject);
yield return null; // Wait for destruction
}
[UnityTest]
public IEnumerator Player_MovesForward_WhenInputApplied()
{
var startPos = _player.transform.position;
_player.Move(Vector3.forward);
yield return new WaitForFixedUpdate();
Assert.Greater(_player.transform.position.z, startPos.z);
}
}
测试协程和异步操作
Edit Mode UnityTest(在 EditorApplication.update 中 yield)
[UnityTest]
public IEnumerator EditorUtility_WhenExecuted_ReturnsSuccess()
{
var utility = RunEditorUtilityInTheBackground();
while (utility.isRunning) { yield return null; }
Assert.IsTrue(utility.isSuccess);
}
Play Mode UnityTest(基于协程)
[UnityTest]
public IEnumerator GameObject_WithRigidBody_WillBeAffectedByPhysics()
{
var go = new GameObject();
go.AddComponent<Rigidbody>();
var originalPosition = go.transform.position.y;
yield return new WaitForFixedUpdate();
Assert.AreNotEqual(originalPosition, go.transform.position.y);
}
可用的 yield 指令(Play Mode)
| Yield 指令 | 效果 |
|---|---|
yield return null | 跳过一帧 |
yield return new WaitForSeconds(t) | 等待 t 秒(缩放时间) |
yield return new WaitForSecondsRealtime(t) | 等待 t 秒的实时 |
yield return new WaitForFixedUpdate() | 等待下一次物理更新 |
yield return new WaitForEndOfFrame() | 等待帧结束 |
yield return new WaitUntil(() => cond) | 等待条件变为 true |
yield return new WaitWhile(() => cond) | 在条件为 true 时等待 |
使用 LogAssert 进行测试
[Test]
public void LogWarning_IsExpected()
{
LogAssert.Expect(LogType.Warning, "Expected warning message");
Debug.LogWarning("Expected warning message");
}
使用 LogAssert.NoUnexpectedReceived() 断言没有发出意外的日志消息。除非这些消息是预期的,否则 Debug.LogError 和 Debug.LogException 会导致测试自动失败。
常见测试模式
Arrange-Act-Assert(AAA)
[Test]
public void Inventory_AddItem_IncreasesCount()
{
// Arrange
var inventory = new Inventory(maxSlots: 10);
var item = new Item("Sword");
// Act
inventory.Add(item);
// Assert
Assert.AreEqual(1, inventory.Count);
}
参数化测试
[TestCase(100, 30, 70)]
[TestCase(100, 100, 0)]
[TestCase(100, 150, 0)]
[TestCase(50, 10, 40)]
public void TakeDamage_CalculatesCorrectly(int maxHp, int damage, int expectedHp)
{
var health = new HealthSystem(maxHp);
health.TakeDamage(damage);
Assert.AreEqual(expectedHp, health.CurrentHealth);
}
测试异常
[Test]
public void Inventory_AddNull_ThrowsException()
{
var inventory = new Inventory(maxSlots: 10);
Assert.Throws<ArgumentNullException>(() =>
{
inventory.Add(null);
});
}
基于场景的测试
[UnityTest]
public IEnumerator Scene_Loads_Successfully()
{
SceneManager.LoadScene("GameScene", LoadSceneMode.Single);
yield return new WaitUntil(() => SceneManager.GetActiveScene().name == "GameScene");
Assert.IsTrue(SceneManager.GetActiveScene().isLoaded);
}
使用伪对象进行测试(依赖注入)
public class FakeAudioService : IAudioService
{
public bool PlaySoundCalled { get; private set; }
public void PlaySound(string name) => PlaySoundCalled = true;
}
[Test]
public void Attack_PlaysSound()
{
var fakeAudio = new FakeAudioService();
var weapon = new Weapon(fakeAudio);
weapon.Attack();
Assert.IsTrue(fakeAudio.PlaySoundCalled);
}
运行测试
从 Test Runner 窗口运行
打开 Window > General > Test Runner。切换 EditMode / PlayMode 复选框。通过 Run All、Run Selected、双击或右键上下文菜单运行测试。使用搜索框或结果图标按钮进行筛选。
从命令行运行(CI/CD)
# Run Edit Mode tests
Unity -runTests -batchmode -projectPath /path/to/project \
-testPlatform EditMode -testResults /path/to/results.xml
# Run Play Mode tests
Unity -runTests -batchmode -projectPath /path/to/project \
-testPlatform PlayMode -testResults /path/to/results.xml
# Run filtered tests by name and category
Unity -runTests -batchmode -projectPath /path/to/project \
-testFilter "HealthSystemTests" -testCategory "Unit;Integration" \
-testResults /path/to/results.xml
命令行参数参考
| 参数 | 描述 |
|---|---|
-runTests | 运行测试所需的标志 |
-testPlatform | EditMode、PlayMode 或任意 BuildTarget 值 |
-testFilter | 以分号分隔的测试名称或正则表达式模式;添加前缀 ! 可取反 |
-testCategory | 以分号分隔的类别名称;添加前缀 ! 可取反 |
-testResults | NUnit XML 结果的输出路径(默认:项目根目录) |
-testNames | FixtureName.TestName 格式的完整测试名称 |
-assemblyNames | 按指定的测试程序集名称筛选 |
-batchmode | 无界面运行(CI/CD 必需) |
-runSynchronously | 在一次编辑器更新中执行所有测试(仅限 EditMode) |
-playerHeartbeatTimeout | 等待播放器心跳的秒数(默认:600) |
IDE 集成
JetBrains Rider 支持直接从 IDE 运行 Unity 测试,并提供完整的调试支持。
反模式
1. 在 [Test] 足以满足需求时使用 [UnityTest]
[UnityTest] 会带来协程开销。对于同步逻辑,请使用 [Test]。
// BAD - unnecessary coroutine overhead
[UnityTest]
public IEnumerator Add_ReturnsSum()
{
var result = Calculator.Add(2, 3);
yield return null;
Assert.AreEqual(5, result);
}
// GOOD - simple synchronous test
[Test]
public void Add_ReturnsSum()
{
var result = Calculator.Add(2, 3);
Assert.AreEqual(5, result);
}
2. 未清理 GameObjects
泄漏的对象会污染其他测试。务必在 [UnityTearDown] 或 [TearDown] 中销毁 GameObjects。在 setup 中创建对象,在 teardown 中销毁对象。
3. 直接测试私有方法
通过公共接口进行测试。如果必须直接测试,请使用 [assembly: InternalsVisibleTo("Tests")]。
4. 硬编码基于时间的等待
请使用 WaitUntil 或 WaitForFixedUpdate,而不是随意指定 WaitForSeconds。
// BAD - brittle, slow
yield return new WaitForSeconds(5f);
Assert.IsTrue(enemy.IsDead);
// GOOD - deterministic
yield return new WaitUntil(() => enemy.IsDead);
Assert.IsTrue(enemy.IsDead);
5. 测试依赖执行顺序
每个测试都必须相互独立。使用 [SetUp] / [TearDown] 来建立干净的状态。
6. 忽略日志错误
意外的 Debug.LogError 调用默认会导致测试失败。对于预期的错误,请使用 LogAssert.Expect();只有在必要时才使用 LogAssert.ignoreFailingMessages = true。
7. 将测试放在生产程序集内
测试程序集使用 UNITY_INCLUDE_TESTS 定义约束,并会从生产构建中排除。切勿将测试代码与游戏代码程序集混在一起。
相关技能
- unity-scripting -- C# 脚本基础知识、MonoBehaviour 生命周期、协程
- unity-editor-tools -- 自定义编辑器、检查器和编辑器扩展