Overview
"I changed this code and now something else is broken..." Sound familiar?
Unity Test Framework is Unity's official testing framework. It integrates NUnit into Unity, allowing you to write and run automated tests. It supports two types of tests: Edit Mode tests and Play Mode tests.
Benefits of Testing
- Early bug detection - Catch bugs immediately after code changes
- Safer refactoring - Verify that existing functionality still works
- Improved code quality - Testable code is well-designed code
- Documentation - Tests demonstrate how code should be used
Edit Mode vs Play Mode Tests
| Aspect | Edit Mode Tests | Play Mode Tests |
|---|---|---|
| Environment | Unity Editor only | Editor or Player |
| Speed | Fast | Slow |
| Use case | Logic testing | Gameplay testing |
| Lifecycle | EditorApplication.update | Awake, Start, Update |
| Access | Editor code + game code | Editor code in Editor, game code only in Player |
Installation
Note: In Unity 2019.2 and later, the Test Framework is installed by default. Manual installation via Package Manager is not required.
Open the Test Runner window via Window > General > Test Runner.
Assembly Definition Setup
If test scripts need to reference production code, you need to configure Assembly Definitions (.asmdef).
Inspector Setup Steps
- Select the Tests.asmdef file
- Open the Assembly Definition References section
- Click the
+button - Select the production code's asmdef (e.g.,
MyGame.Runtime) - Click Apply
asmdef JSON Examples
For Edit Mode tests:
{
"name": "Tests.EditMode",
"references": ["MyGame.Runtime"],
"includePlatforms": ["Editor"],
"defineConstraints": ["UNITY_INCLUDE_TESTS"]
}
For Play Mode tests:
{
"name": "Tests.PlayMode",
"references": ["MyGame.Runtime"],
"includePlatforms": [],
"defineConstraints": ["UNITY_INCLUDE_TESTS"]
}
Important: Play Mode tests require
"includePlatforms": [](empty array). If left as["Editor"], the tests won't be included in builds and cannot run on the Player.
Your First Test
Creating a Test Assembly
- In the Test Runner window, select the
Edit Modetab - Click the
Create EditMode Test Assembly Folderbutton
Creating a Test Script
using NUnit.Framework;
public class MyFirstTest
{
[Test]
public void Add_TwoPlusTwo_ReturnsFour()
{
// Arrange
int a = 2;
int b = 2;
// Act
int result = a + b;
// Assert
Assert.AreEqual(4, result);
}
}
How to Write Tests
Assertions
// Equality tests
Assert.AreEqual(expected, actual);
Assert.AreNotEqual(expected, actual);
// Boolean tests
Assert.IsTrue(condition);
Assert.IsFalse(condition);
// Null tests
Assert.IsNull(obj);
Assert.IsNotNull(obj);
// Exception tests
Assert.Throws<ArgumentException>(() => SomeMethod());
// Constraint Model syntax (recommended for NUnit 3)
Assert.That(result, Is.EqualTo(4));
Assert.That(list, Has.Count.EqualTo(3));
Assert.That(value, Is.InRange(1, 10));
Constraint Model: NUnit 3 recommends the
Assert.Thatsyntax. It's more readable and can express complex conditions.
Test Class Attributes
[TestFixture] // Optional, but required for parameterized test classes
public class MyTest
{
[Test]
public void SomeTest() { }
}
// Generic test class example
[TestFixture(typeof(int))]
[TestFixture(typeof(string))]
public class GenericTest<T>
{
[Test]
public void TypeTest() { }
}
Setup and Teardown
public class MyTest
{
[OneTimeSetUp]
public void OneTimeSetUp() { } // Runs once before all tests
[SetUp]
public void SetUp() { } // Runs before each test
[TearDown]
public void TearDown() { } // Runs after each test
[OneTimeTearDown]
public void OneTimeTearDown() { } // Runs once after all tests
}
Play Mode Setup and Teardown (IEnumerator Version)
public class MyPlayModeTest
{
[UnitySetUp]
public IEnumerator SetUp()
{
// Async setup logic
yield return null;
}
[UnityTearDown]
public IEnumerator TearDown()
{
// Async cleanup logic
yield return null;
}
}
Play Mode Tests
UnityTest Attribute
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using System.Collections;
public class MyPlayModeTest
{
private GameObject testObject;
[UnitySetUp]
public IEnumerator SetUp()
{
testObject = new GameObject("TestObject");
yield return null;
}
[UnityTearDown]
public IEnumerator TearDown()
{
// Ensure cleanup even if the test fails midway
if (testObject != null)
{
Object.Destroy(testObject);
}
yield return null;
}
[UnityTest]
public IEnumerator GameObject_WithRigidbody_FallsDown()
{
// Arrange
testObject.AddComponent<Rigidbody>();
var initialPosition = testObject.transform.position;
// Act - Wait until condition is met (with timeout)
float timeout = 3f;
float elapsed = 0f;
while (testObject.transform.position.y >= initialPosition.y && elapsed < timeout)
{
elapsed += Time.deltaTime;
yield return null;
}
// Assert
Assert.Less(elapsed, timeout, "Timed out waiting for object to fall");
Assert.Less(testObject.transform.position.y, initialPosition.y);
}
}
// Wait pattern comparison
// yield return new WaitForSeconds(1.0f); // Can cause delays in CI/CD
// yield return new WaitForFixedUpdate(); // One physics step (fast)
// for (int i = 0; i < 60; i++) yield return null; // Specific frame count (~1 sec at 60fps)
Testing Multiple Input Values
[TestCase(1, 2, 3)]
[TestCase(2, 3, 5)]
[TestCase(3, 4, 7)]
public void Add_Test(int a, int b, int expected)
{
var calculator = new Calculator();
int result = calculator.Add(a, b);
Assert.AreEqual(expected, result);
}
Categorizing Tests
[Category("Combat")]
[Test]
public void Damage_AppliedCorrectly() { }
[Category("Inventory")]
[Test]
public void Item_AddedToInventory() { }
// Run specific category from command line:
// Unity.exe -batchmode -runTests -testCategory Combat -projectPath /path/to/project
async/await Tests (Unity 2023.1+)
// Available in Unity 2023.1 and later
[Test]
public async Task AsyncOperation_Completes()
{
var result = await SomeAsyncMethod();
Assert.IsNotNull(result);
}
Common Issues and Solutions
Tests Not Being Recognized
- Verify that the Assembly Definition is configured correctly
- Check that test methods have the
[Test]or[UnityTest]attribute - Ensure the test class is
public
Error Logs Cause Test Failures
Use LogAssert.Expect() to expect error logs:
// Correct: Call Expect before the log is emitted
LogAssert.Expect(LogType.Error, "Error message");
Debug.LogError("Error message"); // Expects this log to appear
// Wrong: If the log fires before Expect, the test fails
// Debug.LogError("Error message");
// LogAssert.Expect(LogType.Error, "Error message"); // Too late
Important:
LogAssert.Expect()must be called before the log is emitted. Getting the order wrong will cause the test to fail.
For tests that intentionally trigger errors:
[SetUp]
public void SetUp()
{
// Ignore test failures caused by error logs during the test
LogAssert.ignoreFailingMessages = true;
}
[TearDown]
public void TearDown()
{
LogAssert.ignoreFailingMessages = false; // Always reset
}
Best Practices
- Keep tests small - Test one thing per test
- Use descriptive names - Follow the
MethodName_Condition_ExpectedResultpattern - Follow the AAA pattern - Arrange, Act, Assert
- Keep tests independent - Don't depend on other tests
- Clean up in TearDown - Ensure resources are released even when tests fail
- Refactor tests regularly
CI/CD Test Execution
You can run tests from the command line.
# Windows
Unity.exe -batchmode -runTests -testPlatform EditMode -projectPath /path/to/project
# macOS
/Applications/Unity/Hub/Editor/{version}/Unity.app/Contents/MacOS/Unity -batchmode -runTests -testPlatform EditMode -projectPath /path/to/project
# Output results as XML
Unity.exe -batchmode -runTests -testResults ./results.xml -projectPath /path/to/project
CI/CD integration: Run automated tests with GitHub Actions, Jenkins, GitLab CI, etc. Running tests for every pull request helps maintain quality. Replace
{version}with your Unity version (e.g., 2022.3.10f1).
Checking Test Coverage
Install the Code Coverage package (com.unity.testtools.codecoverage) to visualize how thoroughly your tests cover your code.
- Install Code Coverage from the Package Manager
- Open
Window > Analysis > Code Coverage - Generate a report after running tests
Summary
Unity Test Framework is a powerful tool for improving the quality of Unity projects.
- Edit Mode tests - Best for logic testing, runs fast
- Play Mode tests - Best for gameplay testing
- AAA pattern - Structured with Arrange, Act, Assert
- Setup/Teardown - Define common processing before and after tests
- TestCase attribute - Run the same test with multiple input values
The larger your project gets, the more important testing becomes. Now is the perfect time to start writing tests.