Unity Test Framework Tutorial

by Horia Varlan, CC BY 2.0

Testing is an important part of game development. Manual testing is often the obvious way. But manual testing can be very repetitive. That's why test automation is a very powerful tool for any game development team.

If you’re asking yourself “how do you write tests with unity?”, this tutorial is for you. At the end of it you will know:

  • How to set up the Unity Test Framework
  • How to write your first test with NUnit
  • How to simulate inputs with the Unity InputSystem

Table of Contents

  1. The Sample Project
  2. Installing the Unity Test Framework
  3. Setting up the Test Assembly
  4. A note on the different kind of tests
  5. Your First test
  6. A better test: attacking the skeleton
  7. Scenes as fixtures
  8. Conclusion

The Sample Project

For this tutorial, we will use a sample project available on Github. Download the project by clicking Code > Download Zip.

Open the project with Unity. Open the Scenes/Main.unity scene.

Launch the scene and play the game to have a sense of the gameplay mechanics. The game is a simple action game in which skeletons are chasing you. You can kill them by using your sword (left click), your telekinesis power (right-click), or your shockwave power (hold the middle button then release).

You can reset the game by pressing the r key.

Installing the Unity Test Framework

The Unity Test Framework (UTF) is a package maintained by Unity. It is available via the package manager.

To Install the package, go to Window > Package Manager. Search Test Framework via the search bar, and install the latest version (1.1.22 when writing this tutorial).

Setting up the Test Assembly

Installing the  framework gives you access to a new panel under Window > General > Test Runner:

The runner has 2 main tabs PlayMode and EditMode. PlayMode is for tests that will run while in PlayMode (as if you were playing the game in real-time). EditMode tests run in the Unity Editor and have access to the Editor code besides the game code. It is thus more suitable for Editor extensions.

You want to write tests that are as close as possible to the final game build. So the PlayMode will likely be what you want.

To start writing PlayMode tests, go to your Assets Folder and then click Create PlayMode Test Assembly Folder in the runner. This should create a new Folder, named Tests by default.

Inside the folder, you will find Tests.asmdef which is the assembly definition for your tests. Your tests will need to access the game code. To achieve this, we create another assembly for the rest of the code and then link it in Tests.asmdef.

Go to your Assets folder, and with the contextual menu Create > Assembly Definition. Give a name to the file (for example GameAssembly).

This will associate the entire game code with the new assembly. The code depends on the InputSystem so you need to reference it in the new assembly. Open GameAssembly.asmdef in the inspector, and add Unity.InputSystem in the references. Apply the changes:

Now come back to your Tests assembly. In the inspector add GameAssembly to the Assembly Definition References. Don’t forget to save the modification.

A note on the different kind of tests

Before starting to write any tests for your game you need to ask yourself what kind of tests you want. UTF is just a framework, and only provides the tools to write tests with code. The kind of tests you will write is thus up to you.

If you’re not familiar with testing, you might start with Unit Testing. But there are many more types of tests. Integration tests, and performance tests are a few for example.

If you want to learn more about this you can refer to the following links:

In the rest of the tutorial we will first write a very basic unit test. Then we will move forward with more useful integrations tests.

Your First test

Finally, we can start writing the first test. To do so, we will create a test script via the Test Runner.

Go to your Tests folder and select the Tests assembly definition. In the runner, click Create Test Script in the current folder. Name it TestSuite and open the file. The content should look like this:


using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;

namespace Tests
{
    public class TestSuite
    {
        // A Test behaves as an ordinary method
        [Test]
        public void TestSuiteSimplePasses()
        {
            // Use the Assert class to test conditions
        }

        // A UnityTest behaves like a coroutine in Play Mode.
        // In Edit Mode you can use `yield return null;` to skip a frame.
        [UnityTest]
        public IEnumerator TestSuiteWithEnumeratorPasses()
        {
            // Use the Assert class to test conditions.
            // Use yield to skip a frame.
            yield return null;
        }
    }
}

We will test if a player has the right amount of HP and if using the method applyDamage changes it.

Add a member to the class to reference the player prefab:


public class TestSuite
{
    GameObject playerPrefab = Resources.Load<GameObject>("Player");

    ...
}

Rename the first test to TestPlayerDamage, and instantiate a player:


[Test]
public void TestPlayerDamage()
{
    Vector3 playerPos = Vector3.zero;
    Quaternion playerDir = Quaternion.identity; // the default direction the player is facing is enough
    GameObject player = GameObject.Instantiate(playerPrefab, playerPos, playerDir);
}

The test will now appear in the Test Runner. Even if the test itself is not useful yet, you can already run it. In the runner, select the test in the tree and click Run Selected.

Since the test doesn’t check anything, it passes. Add an assertion to check if the player has 100 HP:

Assert.That(player.GetComponent<Player>().health, Is.EqualTo(100f));

Run this test and see it fail. It's because the default HP for the player prefab is 99, not 100.

Select Player/Resources/Player.prefab and change its Health to 100 via the inspector. Run the test again and this time it turns green.

Now we want to make sure using the applyDamage method on the player changes its HP. To do so, call the method on the player and add another assertion. The complete test now looks like this:


[Test]
public void TestPlayerDamage()
{
    Vector3 playerPos = Vector3.zero;
    Quaternion playerDir = Quaternion.identity; // the default direction the player is facing is enough
    GameObject player = GameObject.Instantiate(playerPrefab, playerPos, playerDir);

    Assert.That(player.GetComponent<Player>().health, Is.EqualTo(100f));

    player.GetComponent<Player>().applyDamage(20f);
    
    Assert.That(player.GetComponent<Player>().health, Is.EqualTo(80f));
}

Run the test again and this time it passes with the new assertion. This means the applyDamage method changes the player HP as expected.

Congratulations! You wrote your very first Unity test.

A better test: attacking the skeleton

The previous test is not very useful, because it tests a very tiny part of the game. For something more useful, we will check if attacking the skeleton affects its HP.

This test has the following requirements:

  • Having access to the player and skeleton prefabs
  • Run without blocking the game loop
  • Being able to simulate player input

To fulfill them, first we need to add the skeleton prefab to the TestSuite class:


public class TestSuite
{
    GameObject playerPrefab = Resources.Load<GameObject>("Player");
    GameObject skeletonPrefab = Resources.Load<GameObject>("ArmedSkeleton");

    ...
}
Secondly and since we will simulate input, we can’t let the test block the game loop. It's because the test will have to span more than 1 frame of the game.
To achieve this, we will add a new test with the UnityTest attribute, which lets us use coroutines:

[UnityTest]
public IEnumerator TestSlashDamagesSkeleton()
{
}

Now let’s instantiate a player and a skeleton:


[UnityTest]
public IEnumerator TestSlashDamagesSkeleton()
{
    Vector3 playerPos = new Vector3(2f, 1f, -1f);
    Quaternion playerDir = Quaternion.identity;
    Vector3 skeletonPos = new Vector3(2f, 0f, 1f);
    Quaternion skeletonDir = Quaternion.LookRotation(new Vector3(0f, 0f, -1f), Vector3.up);

    GameObject player = GameObject.Instantiate(playerPrefab, playerPos, playerDir);
    GameObject skeleton = GameObject.Instantiate(skeletonPrefab, skeletonPos, skeletonDir);

    yield return null;
}

Note that yielding a value is necessary because UnityTest requires an enumerator. And since we're not using any wait yet, we simply yield a null value.

Additionally, the skeleton depends on a player object, so we give it one:


skeleton.GetComponent<Skeleton>().player = player.GetComponent<Player>();

To allow our test to use and simulate inputs, we need to inherit from the InputTestFixture class:


...
using UnityEngine.InputSystem;

namespace Tests
{
    public class TestSuite: InputTestFixture
    {
  		...
    }
}

However this class is not available by default. So we need to add the relevant references to our tests assembly.

Go to your tests folder and select the Tests assembly definition.
Under Assembly Definition References, add Unity.InputSystem and Unity.InputSystem.TestFramework. Apply the modifications:

But this won’t be enough. As the InputSystem documentation tells us, we need to add the package to the project manifest’s testables.

Open Packages/manifest.json, and below dependencies, add a testables section:


{
  "dependencies": {
      ...
  },
  "testables": ["com.unity.inputsystem"]
}

After this, references to InputTestFixture shouldn’t raise compilation errors. But we need further modifications to simulate input. We have to add a mouse and a keyboard device to our InputSystem. We do that by overriding the Setup method of our class:


public class TestSuite: InputTestFixture
{
    GameObject playerPrefab = Resources.Load<GameObject>("Player");
    GameObject skeletonPrefab = Resources.Load<GameObject>("ArmedSkeleton");
    Mouse mouse;
    Keyboard keyboard;

    public override void Setup()
    {
        base.Setup();
        mouse = InputSystem.AddDevice<Mouse>();
        keyboard = InputSystem.AddDevice<Keyboard>();
    }

    ...
}

Now we can write the important part of the test! We will check the initial skeleton’s HP, trigger a sword slash, and check if the HP have changed. The entire test then looks like this:


[UnityTest]
public IEnumerator TestSlashDamagesSkeleton()
{
    Vector3 playerPos = new Vector3(2f, 1f, -1f);
    Quaternion playerDir = Quaternion.identity;
    Vector3 skeletonPos = new Vector3(2f, 0f, 1f);
    Quaternion skeletonDir = Quaternion.LookRotation(new Vector3(0f, 0f, -1f), Vector3.up);

    GameObject player = GameObject.Instantiate(playerPrefab, playerPos, playerDir);
    GameObject skeleton = GameObject.Instantiate(skeletonPrefab, skeletonPos, skeletonDir);
    skeleton.GetComponent<Skeleton>().player = player.GetComponent<Player>();

    Assert.That(skeleton.GetComponent<Skeleton>().health, Is.EqualTo(100f));

    Press(mouse.leftButton);
    yield return new WaitForSeconds(0.1f);
    Release(mouse.leftButton);
    yield return new WaitForSeconds(3f);

    Assert.That(skeleton.GetComponent<Skeleton>().health, Is.EqualTo(80f));
}
We did it! We managed to write a test with input emulation. These kinds of tests are useful because they inform you about essential parts of the game. If for any reason the sword stops damaging skeletons, the test will catch it.

Scenes as fixtures

Because the player acts as a camera, we don't notice that both the player and the skeleton are in free fall. Since the default testing scene has no floor, this situation can lead to test flakiness.

To improve that we will use a test scene, providing a default environment. This scene is already available in the project as Scenes/Sandbox.unity, we just have to load it in our Setup method:


...
using UnityEngine.SceneManagement;

namespace Tests
{
    public class TestSuite: InputTestFixture
    {
        ...
        
        public override void Setup()
        {
            base.Setup();
            SceneManager.LoadScene("Scenes/Sandbox");

            ...
        }
    }
}

Note that the scene has to be available in the build settings. Or else you’ll see the following error:

Scene 'Scenes/Sandbox' couldn't be loaded because it has not been added to the build settings or the AssetBundle has not been loaded.
To add a scene to the build settings use the menu File->Build Settings...

You can create similar scenes in advance to test the behavior of your objects, and then load them from the tests.

Optionally and to make things slightly better, we will use the scene’s camera instead of the player’s camera. This makes it easier to see what’s happening. To do that, we tell the Player prefab we won’t use its camera, in the Setup method:


playerPrefab.GetComponent<Player>().activeCam = false;

We will also disable the skeleton in the test method since we’re only testing damage and not its behavior:


skeleton.GetComponent<Skeleton>().enabled = false;

Finally, here is the entire TestSuite.cs file:


using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using UnityEngine.InputSystem;
using UnityEngine.SceneManagement;

namespace Tests
{
    public class TestSuite: InputTestFixture
    {
        GameObject playerPrefab   = Resources.Load<GameObject>("Player");
        GameObject skeletonPrefab = Resources.Load<GameObject>("ArmedSkeleton");
        Mouse mouse;
        Keyboard keyboard;

        public override void Setup()
        {
            base.Setup();
            SceneManager.LoadScene("Scenes/Sandbox");
            playerPrefab.GetComponent<Player>().activeCam = false;

            mouse = InputSystem.AddDevice<Mouse>();
            keyboard = InputSystem.AddDevice<Keyboard>();
        }

        [Test]
        public void TestPlayerDamage()
        {
            Vector3 playerPos = Vector3.zero;
            Quaternion playerDir = Quaternion.identity; // the default direction the player is facing is enough
            GameObject player = GameObject.Instantiate(playerPrefab, playerPos, playerDir);

            Assert.That(player.GetComponent<Player>().health, Is.EqualTo(100f));

            player.GetComponent<Player>().applyDamage(20f);

            Assert.That(player.GetComponent<Player>().health, Is.EqualTo(80f));
        }

        [UnityTest]
        public IEnumerator TestSlashDamagesSkeleton()
        {
            Vector3 playerPos = new Vector3(2f, 1f, -1f);
            Quaternion playerDir = Quaternion.identity;
            Vector3 skeletonPos = new Vector3(2f, 0f, 1f);
            Quaternion skeletonDir = Quaternion.LookRotation(new Vector3(0f, 0f, -1f), Vector3.up);

            GameObject player = GameObject.Instantiate(playerPrefab, playerPos, playerDir);
            GameObject skeleton = GameObject.Instantiate(skeletonPrefab, skeletonPos, skeletonDir);
            skeleton.GetComponent<Skeleton>().player = player.GetComponent<Player>();
            skeleton.GetComponent<Skeleton>().enabled = false;

            Assert.That(skeleton.GetComponent<Skeleton>().health, Is.EqualTo(100f));

            Press(mouse.leftButton);
            yield return new WaitForSeconds(0.1f);
            Release(mouse.leftButton);
            yield return new WaitForSeconds(3f);

            Assert.That(skeleton.GetComponent<Skeleton>().health, Is.EqualTo(80f));
        }
    }
}

Conclusion

Getting started with the Unity Test Framework is not a simple task. However I hope you learned how to write your first meaningful tests with this tutorial.

Running these tests from the editor might be manageable for now, but with the time you will reach a point when you will need to run all your tests as part of a building pipeline. Once this is the case, the next step is to use Game Conductor to scale up your testing!

 

External links:

Unity Test Framework documentation.

InputSystem documentation on input testing.

This article was updated on May 14, 2021