Writing Test Cases
Your one-step guide to writing test cases in Unity.
Test Modes
Tests in Unity are segregated into two unique execution contexts:
Editor Mode: Runs directly in the Editor without entering Play Mode. This is fast as it avoids execution context switches, making it ideal for pure C# and logic that doesn’t require interfacing with scene objects.
Play Mode: Executed in Play Mode with a running game loop. This is ideal for runtime tests that require interfacing with MonoBehaviours, scenes, physics, inputs, etc.
Assembly Setups
Use Assembly Definition Files (.asmdef) to keep test code completely isolated from production code, and one another. They can be created from the usual file creation menu:

Alternatively, you may automatically create a folder along with an Assembly Definition File within it using:

Notice the following internal folder structure of the “Tests” folder as mentioned above, grouped into Editor and Play Mode tests:

In these individual folders, there are unique folders for different systems and test code that resides in them. Each of their respective test suites are kept isolated through the use of assembly files, and no code spillover happens as a result.


Note that all scripts in the same folder as an Assembly Definition File, belongs to that Assembly!
Editor-Only Test Assemblies
This is the general look of an Assembly Definition File meant for Editor Tests.

If your test suite needs to reference other assemblies such as Mirror (to use functions from it), make sure to add its assembly definition file to Assembly Definition References. There are certain things to take note of to make sure that the assembly is visible to the Unity Test Runner (Edit Mode):
Under Platforms, ensure that only “Editor” is checked.
Under Assembly Definition References, ensure that “UnityEditor.TestRunner” is present.
Under Define Constraints, ensure that “UNITY_INCLUDE_TESTS” is present. This signals to Unity that the assembly contains tests, and that they will not be present in a full build.
Play Mode Test Assemblies
This is the general look of an Assembly Definition File meant for Play Mode Tests.

If your test suite needs to reference other assemblies such as Mirror (to use functions from it), make sure to add its assembly definition file to Assembly Definition References. There are certain things to take note of to make sure that the assembly is visible to the Unity Test Runner (Play Mode):
Enable “Override References”.
Make sure to reference “nunit.framework.dll”’ under Assembly References.
Under Assembly Definition References, ensure that “UnityEngine.TestRunner” is present.
Under Platforms, ensure that your test platforms of choice are selected.
Under Define Constraints, ensure that “UNITY_INCLUDE_TESTS” is present. This signals to Unity that the assembly contains tests, and that they will not be present in a full build.
The Test Runner
The test runner is the control panel for running tests within Unity. You may open it as follows:

Here, you may switch between Edit Mode and Play Mode tests. Note that all tests scattered across your entire project can be found here, grouped based on their respective assembly definition files.


To run tests, whether as an assembly, class or individually, simply double click its entry or right click and select “Run”.
Execution Order
All tests in a suite follow a pre-defined execution order, defined by built-in Unity attributes. There are two types of attribute types:
Per Test Suite
[OneTimeSetUp]: Runs before any test method in the class is executed. Good for expensive initialization shared across all tests (e.g., loading large assets, creating test environments).
[OneTimeTearDown]: Runs after all test methods in the class have finished. Good for cleanup (e.g., destroying objects, releasing memory).
Per Test
[Setup]: Runs before each test. Typical place to initialize variables, create fresh GameObjects, or reset state.
[TearDown]: Runs after each test. Cleans up what [Setup] prepared.
[UnitySetUp]: Runs before each test but allows coroutines (returns IEnumerator). Useful when your setup needs to wait for frames, physics, or async behavior.
[UnityTearDown]: Runs after each test and also allows coroutines. Good for cleanup that needs to yield (like waiting for object destruction or scene unloading).
[Test]: Marks a function as a test that executes synchronously in one frame. Methods must return void.
[UnityTest]: Marks a function as a test that can execute “asynchronously” using coroutines (IEnumerator). Can yield (wait for frames, seconds, async operations). Useful for testing behaviors over time (movement, animations, async loading).
Creating Tests
Common Confusion
Notice how tests can be attributed with either [Test] or [UnityTest].
[Test]: A test that simply executes serially and exits. Any form of yielding/waiting behaviour is not supported.
[UnityTest]: An alternative to the above Test attribute which allows for instruction yielding back to the framework. Once the yielding is complete, the test run continues. For example, if you yield return null, you skip a frame. Note that such functions must have a return type of IEnumerator. Both WaitForSeconds and WaitForFixedUpdate are supported here.
Both can be used for either execution contexts.
General View
A general view into the inner workings of a test script is as follows:

All implemented test classes/tests must follow the same layout as seen above, allowing for better accountability/documentation. This means:
All classes are labelled with [TextFixture], along with [Category] and [Description] attributes.
All tests are labelled with [Description] and [Author] attributes. Notice that a verbose test name makes certain descriptions redundant. However, this is still mandatory for standardization whilst allowing for extra information to be highlighted to other developers.
Parameterized Tests

Parameterized tests allow us to pass an arbitrary amount of parameters into any singular test, allowing it to auto-rerun based on the number of incoming parameters. This allows the same test logic to be validated against multiple inputs without duplicating code.
Other Attributes
Additional useful built-in attributes are listed here. Feel free to use them accordingly as needed.
[UnityPlatform]: Use this attribute to define a specific set of platforms you wish to restrict your tests from running on. Note that this can be used on a test suite level, or aggregated with commas as needed. Furthermore, platform exclusions on the assembly level overrides this attribute accordingly.

[OrderBy]: By default, Unity discovers tests and runs them in whatever order it deems fit. Whilst independent/isolated testing is generally preferred, developers may sometimes wish to avoid repeated setups or initializations for test cases centered around a common system. Hence, the OrderBy attribute allows us to do just that. Note that this isn’t recommended and should only be done where calls get really slow/expensive otherwise.

Dependency-Based Test Cases
Dependency-based test cases, as the name suggests, denote tests that require dependencies to be in-place before tests can be executed. Such examples include
Out-of-engine connection setups.
Database creations.
Server/client launches.
Tweaking of settings to simulate different application environments.
All test cases in the same fixture thus rely upon these dependencies for their respective executions. In such cases, it is generally recommended to follow this setup:
Group your test cases into classes (also known as test fixtures). Fixtures are groups of tests that run together under a shared [OneTimeSetUp] and [OneTimeTearDown] umbrella.
Use [OneTimeSetUp] to initialize your dependencies. For example, you could launch a server with specific port settings along with mock clients.
Write your test cases with [UnityTest] or [Test].
Use [OneTimeTearDown] to clean-up your dependencies accordingly.
Test Writing Guidelines
Tests should follow a strict set of guidelines to ensure brevity and avoid wasting developer time.
Test files should be kept as clean as possible. We’re only calling functions from our engine’s systems and verifying if they’re working as intended. If you’re writing extra wrapper code to pass inputs around, hold temporary variables, etc., you’re doing something wrong.
Backend tests should be concise and focus on core functionality. For example, launching a server should be as simple as calling NetworkCore.LaunchServer(Parameters). Connecting to this server should be as simple as Client.ConnectToServer(Parameters). This in turn also tests how well your backend code is structured/architectured.
Do not write tests that simulate user interaction with UI. This is a major time sink, especially with how fast scenes/user workflows are currently changing. Leave this for QA testing. Instead, simulate the underlying code flow that drives the UI interactions.
Always use Asserts for actual pass/fail criteria in your tests. Do not use Debug.LogError or Try/Catch blocks as these don’t actually record as test failures in the Unity Test Framework.
At times, we may require a function to test multiple inputs, such as string processing. In such cases, use parameterized tests as highlighted above instead of duplicating functions or creating for-loops.
Networking Tests
While running tests from the Unity Editor/CLI, we naturally assume the role of clients. This means that we launch a server application in [OneTimeSetUp], and each following test simulates a piece of functionality as run by ordinary users of our application.
To run tests from the POV of servers, sub-servers and sub-clients, we will have to rely on IPC calls. This is a future feature that is currently WIP.
Network Sandboxing
Building the entire project whenever we update a piece of logic for our system(s) could be immensely time draining. This is especially so when it comes to server/client architectures where we only need to test specific scenes or networking components in isolation. By setting up automated scene builds, we can avoid the overhead of full project builds whilst validating workflows - quickly building our projects with only the necessary scenes (e.g., server and client) and performing testing with them directly.
Note that this can only be done in EditMode tests as the Unity Editor has to be present for builds.

Async Tests
Coroutines vs Task-Based Async/Await
Coroutines are driven by Unity’s scheduler, which drives methods frame by frame as needed. However, as we know, there is no real threading happening as code is simply pausing and unpausing in Unity’s update loop. This means that everything is still happening on the main thread.
C# allows for true concurrent task executions (with .NET’s thread pool scheduler) using methods marked with async. Such methods may not execute on Unity’s main thread, which is an important distinction as opposed to Coroutines. At each await call within an async function, control is returned to the caller and the awaited task completes later, at which continuation happens.

Note that you should not use await on a Task and proceed to utilize the Unity API afterwards (Transform, GameObjects, Physics, etc.). This is because you’re no longer on Unity’s main thread after an await returns (this is decided at random by the C# runtime scheduler) and exceptions will occur if you try to do so.
UniTask
UniTask is an alternative to Tasks that sits right between Unity Coroutines and C#’s Async/Await functionality. As opposed to Tasks that work exclusively with non-Unity constructs, UniTasks allow you to seamlessly integrate with Unity by enforcing task execution on its main thread. Hence, after an await, we can simply utilize the Unity API without worrying.

UniTask is completely compatible with Coroutine-based functions. It is not uncommon for entire codebases to have their Coroutines replaced with UniTasks!

Note that UniTask works on Unity’s main thread by default. Hence, it is not threaded unless you explicitly tell it to do so with a combination of UniTask.RunOnThreadPool / UniTask.SwitchToMainThread, allowing tasks to first execute on a background thread and forcing it onto Unity’s main thread after!

When to use which?
Coroutines: If your test logic is tied to frames and you want guaranteed main thread execution (spawning GameObjects, waiting for Physics, etc.).
Task: If you’re testing non-Unity related services relating to web requests, file IO, networking, etc.
UniTask: If you wish to have an interface that is (true) threading compatible whilst retaining flexibility with Unity’s services.
Group your test cases into classes (also known as test fixtures). Fixtures are groups of tests that run together under a shared [OneTimeSetUp] and [OneTimeTearDown] umbrella.
Use [OneTimeSetUp] to initialize your dependencies. For example, you could launch a server with specific port settings along with mock clients.
Write your test cases with [UnityTest] or [Test].
Use [OneTimeTearDown] to clean-up your dependencies accordingly.
Last updated