Unity Testing¶
Unity Test Framework (UTF) 2.x¶
Unity uses NUnit as its testing foundation, extended with Unity-specific attributes and runners.
Test Types¶
Edit Mode Tests¶
Run without entering play mode. Test pure C# logic, ScriptableObject behavior, and editor tools.
using NUnit.Framework;
public class HealthTests
{
[Test]
public void TakeDamage_ReducesHealth()
{
var health = new HealthComponent { Current = 100, Max = 100 };
health.TakeDamage(25);
Assert.AreEqual(75, health.Current);
}
[Test]
public void TakeDamage_DoesNotGoBelowZero()
{
var health = new HealthComponent { Current = 10, Max = 100 };
health.TakeDamage(50);
Assert.AreEqual(0, health.Current);
}
[Test]
public void Heal_DoesNotExceedMax()
{
var health = new HealthComponent { Current = 80, Max = 100 };
health.Heal(50);
Assert.AreEqual(100, health.Current);
}
}
Play Mode Tests¶
Run inside play mode with scene loading, physics, and full MonoBehaviour lifecycle.
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
public class PlayerMovementTests
{
[UnityTest]
public IEnumerator Player_MovesForward_WhenInputApplied()
{
var playerGO = new GameObject("Player");
var controller = playerGO.AddComponent<PlayerController>();
Vector3 startPos = playerGO.transform.position;
controller.SimulateInput(Vector2.up);
yield return new WaitForSeconds(0.5f);
Assert.Greater(playerGO.transform.position.z, startPos.z);
Object.Destroy(playerGO);
}
}
Async Tests with Awaitable¶
Use async tests for modern Unity code:
[Test]
public async System.Threading.Tasks.Task AsyncOperation_CompletesSuccessfully()
{
var loader = new AssetLoader();
var result = await loader.LoadConfigAsync();
Assert.IsNotNull(result);
Assert.AreEqual("default", result.Name);
}
Assertions¶
Assert.AreEqual(expected, actual);
Assert.AreNotEqual(unexpected, actual);
Assert.IsTrue(condition);
Assert.IsFalse(condition);
Assert.IsNull(obj);
Assert.IsNotNull(obj);
Assert.Greater(a, b);
Assert.Less(a, b);
Assert.That(value, Is.InRange(min, max));
Assert.Throws<ArgumentException>(() => DoSomething());
// Float comparison with tolerance
Assert.AreEqual(1.0f, actual, 0.001f);
Test Fixtures¶
public class InventoryTests
{
private Inventory _inventory;
[SetUp]
public void SetUp()
{
_inventory = new Inventory(maxSlots: 10);
}
[TearDown]
public void TearDown()
{
_inventory = null;
}
[Test]
public void AddItem_IncreasesCount()
{
_inventory.Add(new Item("Sword"));
Assert.AreEqual(1, _inventory.Count);
}
[Test]
public void AddItem_WhenFull_ReturnsFalse()
{
for (int i = 0; i < 10; i++)
_inventory.Add(new Item($"Item{i}"));
bool result = _inventory.Add(new Item("Extra"));
Assert.IsFalse(result);
}
}
Assembly Definition Setup¶
Tests need their own assembly definitions:
Edit Mode Tests (Tests/EditMode/MyGame.Tests.EditMode.asmdef)¶
{
"name": "MyGame.Tests.EditMode",
"references": ["MyGame.Runtime"],
"includePlatforms": ["Editor"],
"optionalUnityReferences": ["TestAssemblies"]
}
Play Mode Tests (Tests/PlayMode/MyGame.Tests.PlayMode.asmdef)¶
{
"name": "MyGame.Tests.PlayMode",
"references": ["MyGame.Runtime", "UnityEngine.TestRunner", "UnityEditor.TestRunner"],
"includePlatforms": [],
"optionalUnityReferences": ["TestAssemblies"]
}
Mocking Patterns¶
Use interfaces and dependency injection to make code testable:
public interface IAudioService
{
void PlaySFX(string name);
}
public class PlayerCombat
{
private readonly IAudioService _audio;
public PlayerCombat(IAudioService audio)
{
_audio = audio;
}
public void Attack(IDamageable target)
{
target.TakeDamage(10);
_audio.PlaySFX("sword_swing");
}
}
For automated mocking, use NSubstitute:
var audioMock = Substitute.For<IAudioService>();
var target = Substitute.For<IDamageable>();
var combat = new PlayerCombat(audioMock);
combat.Attack(target);
target.Received(1).TakeDamage(10);
audioMock.Received(1).PlaySFX("sword_swing");
Running Tests¶
- Test Runner Window: Window > General > Test Runner
- Command Line:
Unity.exe -runTests -testResults results.xml -testPlatform EditMode - CI Integration: Use the
-batchmode -nographicsflags for headless test execution