unity-testing

Installation
SKILL.md

Unity Test Framework

Test Framework Overview

The Unity Test Framework (UTF) is Unity's built-in testing solution, integrating a custom version of NUnit (based on NUnit 3.5) adapted for Unity. It enables testing C# code in Edit Mode, Play Mode, and on target platforms (Standalone, Android, iOS).

UTF extends NUnit with Unity-specific attributes ([UnityTest], [UnitySetUp], [UnityTearDown]) that support coroutines, yield instructions, and frame-based execution.

Key capabilities:

  • Edit Mode tests for editor extensions and pure logic
  • Play Mode tests for gameplay, physics, and coroutine-based code
  • Standalone player test builds for platform-specific validation
  • Command-line execution for CI/CD pipelines
  • NUnit XML result output

Source: Unity Test Framework 2.0 Manual


Setup and Configuration

Installing the Test Framework

UTF ships with Unity by default. Open the Test Runner via Window > General > Test Runner.

Creating a Test Assembly

Test code must live in its own assembly definition (.asmdef) that references NUnit.

Create via Window > General > Test Runner (click Create a new Test Assembly Folder) or Assets > Create > Testing > Test Assembly Folder.

This creates a Tests folder with an .asmdef file preconfigured with references to:

  • nunit.framework.dll
  • UnityEngine.TestRunner
  • UnityEditor.TestRunner (Edit Mode only)

Edit Mode Assembly Definition (.asmdef)

{
    "name": "Tests",
    "references": [
        "UnityEngine.TestRunner",
        "UnityEditor.TestRunner"
    ],
    "includePlatforms": ["Editor"],
    "defineConstraints": ["UNITY_INCLUDE_TESTS"],
    "autoReferenced": false
}

Play Mode Assembly Definition (.asmdef)

{
    "name": "Tests.PlayMode",
    "references": [
        "UnityEngine.TestRunner",
        "UnityEditor.TestRunner",
        "MyGameAssembly"
    ],
    "includePlatforms": [],
    "optionalUnityReferences": ["TestAssemblies"],
    "defineConstraints": ["UNITY_INCLUDE_TESTS"],
    "autoReferenced": false
}

Important: To test your game code, add a reference to the assembly containing the code under test in the references array.

Source: Creating Test Assemblies


Edit Mode vs Play Mode Tests

Aspect Edit Mode Play Mode
Execution context EditorApplication.update callback loop Coroutine on a MonoBehaviour
Editor access Full Editor + game code Game code only (unless in Editor)
Coroutine support yield return null skips one update Full yield instructions (WaitForSeconds, etc.)
Platform targets Editor only Editor Play Mode + standalone players
Use cases Editor extensions, pure logic, serialization Physics, gameplay, MonoBehaviour lifecycle
Assembly platform "includePlatforms": ["Editor"] "includePlatforms": []

When to use Edit Mode:

  • Testing editor tools, custom inspectors, ScriptableObject logic
  • Pure C# logic that does not depend on runtime systems
  • Tests that need access to UnityEditor APIs

When to use Play Mode:

  • Testing MonoBehaviour lifecycle (Awake, Start, Update)
  • Physics interactions requiring WaitForFixedUpdate
  • Coroutine and async workflows
  • Scene loading and object instantiation at runtime

Best practice: Use NUnit [Test] instead of [UnityTest] unless you need yield instructions. [Test] runs faster since it does not require coroutine overhead.

Source: Edit Mode vs Play Mode Tests


Writing Tests

Create test scripts via Assets > Create > Testing > C# Test Script or using the Test Runner's Create a new Test Script button.

Basic Test Structure

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 Attributes Reference

Attribute Purpose
[Test] Marks a method as a synchronous test
[TestFixture] Marks a class as containing tests
[SetUp] Runs before each test method
[TearDown] Runs after each test method
[OneTimeSetUp] Runs once before all tests in the fixture
[OneTimeTearDown] Runs once after all tests in the fixture
[TestCase(args)] Parameterized test with inline data
[Values(args)] Provides values for a test parameter
[Category("name")] Categorizes tests for filtering
[Ignore("reason")] Skips a test with a reason
[Timeout(ms)] Sets a timeout for the test

Unity-Specific Attributes

Attribute Purpose
[UnityTest] Coroutine-based test supporting yield instructions
[UnitySetUp] Coroutine-based setup (supports yield)
[UnityTearDown] Coroutine-based teardown (supports yield)
[RequiresPlayMode] Forces test to run in Play Mode (true) or Edit Mode (false)
[UnityPlatform] Restricts test to specific platforms
[TestMustExpectAllLogs] Requires all log messages to be expected
[PrebuildSetup] Runs setup before player build
[PostBuildCleanup] Runs cleanup after player build
[ConditionalIgnore] Conditionally ignores tests
[TestRunCallback] Subscribes to test progress updates

Common Assertions

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

See references/test-framework.md for the full assertion API reference.

Source: Creating Tests


Testing MonoBehaviours

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);
    }
}

Testing Coroutines and Async

Edit Mode UnityTest (yields in EditorApplication.update)

[UnityTest]
public IEnumerator EditorUtility_WhenExecuted_ReturnsSuccess()
{
    var utility = RunEditorUtilityInTheBackground();
    while (utility.isRunning) { yield return null; }
    Assert.IsTrue(utility.isSuccess);
}

Play Mode UnityTest (coroutine-based)

[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);
}

Available Yield Instructions (Play Mode)

Yield Instruction Effect
yield return null Skip one frame
yield return new WaitForSeconds(t) Wait for t seconds (scaled time)
yield return new WaitForSecondsRealtime(t) Wait for t real-time seconds
yield return new WaitForFixedUpdate() Wait for next physics update
yield return new WaitForEndOfFrame() Wait until end of frame
yield return new WaitUntil(() => cond) Wait until condition is true
yield return new WaitWhile(() => cond) Wait while condition is true

Testing with LogAssert

[Test]
public void LogWarning_IsExpected()
{
    LogAssert.Expect(LogType.Warning, "Expected warning message");
    Debug.LogWarning("Expected warning message");
}

Use LogAssert.NoUnexpectedReceived() to assert no unexpected log messages were emitted. Debug.LogError and Debug.LogException cause automatic test failure unless expected.


Common Test Patterns

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);
}

Parameterized Tests

[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);
}

Testing Exceptions

[Test]
public void Inventory_AddNull_ThrowsException()
{
    var inventory = new Inventory(maxSlots: 10);

    Assert.Throws<ArgumentNullException>(() =>
    {
        inventory.Add(null);
    });
}

Scene-Based Testing

[UnityTest]
public IEnumerator Scene_Loads_Successfully()
{
    SceneManager.LoadScene("GameScene", LoadSceneMode.Single);
    yield return new WaitUntil(() => SceneManager.GetActiveScene().name == "GameScene");
    Assert.IsTrue(SceneManager.GetActiveScene().isLoaded);
}

Testing with Fakes (Dependency Injection)

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);
}

Running Tests

From the Test Runner Window

Open Window > General > Test Runner. Toggle EditMode / PlayMode checkboxes. Run tests via Run All, Run Selected, double-click, or right-click context menu. Filter using the search box or result icon buttons.

From the Command Line (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

Command-Line Arguments Reference

Argument Description
-runTests Required flag to run tests
-testPlatform EditMode, PlayMode, or any BuildTarget value
-testFilter Semicolon-separated test names or regex pattern; prefix with ! to negate
-testCategory Semicolon-separated category names; prefix with ! to negate
-testResults Output path for NUnit XML results (default: project root)
-testNames Full test names in FixtureName.TestName format
-assemblyNames Filter by specific test assembly names
-batchmode Run without UI (required for CI/CD)
-runSynchronously Execute all tests in one editor update (EditMode only)
-playerHeartbeatTimeout Seconds to wait for player heartbeat (default: 600)

IDE Integration

JetBrains Rider supports running Unity tests directly from the IDE with full debugging support.

Source: Running Tests, Command Line Reference


Anti-Patterns

1. Using [UnityTest] when [Test] suffices

[UnityTest] adds coroutine overhead. Use [Test] for synchronous logic.

// 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. Not cleaning up GameObjects

Leaked objects contaminate other tests. Always destroy GameObjects in [UnityTearDown] or [TearDown]. Create objects in setup, destroy in teardown.

3. Testing private methods directly

Test through public interfaces. If you must, use [assembly: InternalsVisibleTo("Tests")].

4. Hardcoding time-based waits

Use WaitUntil or WaitForFixedUpdate instead of arbitrary 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. Tests depending on execution order

Each test must be independent. Use [SetUp] / [TearDown] to establish clean state.

6. Ignoring log errors

Unexpected Debug.LogError calls cause test failures by default. Use LogAssert.Expect() for expected errors or LogAssert.ignoreFailingMessages = true only when necessary.

7. Placing tests in production assemblies

Test assemblies use UNITY_INCLUDE_TESTS define constraint and are excluded from production builds. Never mix test code with game code assemblies.


Related Skills

  • unity-scripting -- C# scripting fundamentals, MonoBehaviour lifecycle, coroutines
  • unity-editor-tools -- Custom editors, inspectors, and editor extensions

Additional Resources

Weekly Installs
7
GitHub Stars
8
First Seen
Mar 19, 2026
Installed on
amp6
cline6
opencode6
cursor6
kimi-cli6
warp6