Unity Tutorials Beta

Learn how to create games in Unity, a powerful and popular game engine.

Introduction To Unity Unit Testing

Learn all about how Unit Tests in Unity work and how to use them in your projects in this great tutorial.

5/5 15 Ratings

Version

  • C# 7.2, Unity 2018.3, Unity

Curious how unit testing works in Unity? Unfamiliar with how unit testing works in general? If you answered yes to these questions then this tutorial is for you. In this tutorial you’ll learn the following about unit testing:

  • What it is
  • The value it offers
  • Pros and cons
  • How it works in Unity using the Test Runner
  • Writing and running unit tests that pass
Note: This tutorial assumes you are familiar with C# and basic Unity development. If you are new to Unity, check out our other Unity tutorials.

What Is a Unit Test?

Before diving into code, it’s important to have a solid understanding of what unit testing is. Put simply, a unit test is…a unit test. :]

A unit test is (ideally) for testing a single “unit” of code. Exactly what makes up a “unit” varies, but the important thing to keep in mind is that a unit test should be testing exactly one ‘thing’ at a time.

You should design a unit test to validate that a small, logical, snippet of code performs exactly as you expect it to in a specific scenario. This might be hard to grasp before you’ve written any unit tests, so consider this example:

You’ve written a method that allows the user to input a name. You wrote the method so there are no numbers allowed in the name, and the name can only be ten characters or less. Your method intercepts each keystroke and adds that character to the name field as shown below:

public string name = ""
public void UpdateNameWithCharacter(char: character)
{
    // 1
    if (!Char.IsLetter(char))
    {
        return;
    }

    // 2
    if (name.Length > 10)
    {
        return;
    }

    // 3
    name += character;
}

This is what’s going on here:

  1. If the character is not a letter, this code exits the function early and doesn’t add the character to the string.
  2. If the length of the name is ten characters or greater, it prevents the user from adding another character.
  3. Once those two tests pass, the code adds the character to the end of the name.

This method is testable because it does a “unit” of work. Unit tests enforce the method’s logic.

Looking at Example Unit Tests

How would you write unit tests for the UpdateNameWithCharacter method?

Before you get started with implementing these unit tests, you’ll need to think carefully about what the tests are doing, and come up with names for them.

Take a look at the sample test method names below. The names should make it clear what’s being tested:

UpdateNameDoesntAllowCharacterAddingToNameIfNameIsTenOrMoreCharactersInLength

UpdateNameAllowsLettersToBeAddedToName

UpdateNameDoesntAllowNonLettersToBeAddedToName

From these test method names, you can see that you’re testing that the “unit” of work being performed by UpdateNameWithCharacter is doing what it should. These test names might seem long and very specific, but this is helpful.

Every unit test you write makes up part of a test suite. A test suite houses all unit tests related to a logical grouping of functionality (like your combat unit tests). If any individual test in a test suite fails, the entire test suite fails.

unit testing: test suite

Starting Up the Game

Open the Crashteroids Starter project (you can find the Download Materials button at the top or bottom of this tutorial), and open the Game scene in Assets / RW / Scenes.

unit testing: game one

Click Play to get Crashteroids started, and then click the Start Game button. Use the left and right arrow keys to move the spaceship left and right.

Press the spacebar to fire a laser. If a laser hits an asteroid, the score will go up by one. If an asteroid hits the ship, the ship explodes and it’s game over (with the option to start again).

unit testing: game two

Try playing for a while, and then make sure the ship gets hit by an asteroid to see that Game Over triggers.

unit testing: game three

Getting Started with the Unity Test Runner

Now that you know how the game works, it’s time to write unit tests to ensure everything behaves as it should. This way if you (or anyone else) decides to update the game, you can be confident in knowing that the update didn’t break anything that was working before.

To write tests, you first need to know about Unity’s Test Runner. The Test Runner lets you run tests and see if they pass. To open the Unity Test Runner, choose Window ▸ General ▸ Test Runner.

After the Test Runner opens as a new window, you can make life easier by clicking the Test Runner window and dragging it next to your Scene window.

Setting Up NUnit and Test Folders

Test Runner is the unit testing feature provided by Unity — but it utilizes the NUnit framework. As you get more serious about writing unit tests, you should consider reading the wiki on NUnit to learn more. For now, everything you need to know will be covered here.

In order to run tests, you first need to create a test folder to hold your test classes.

In the Project window, select the RW folder. Look at the Test Runner window and make sure PlayMode is selected.

Click the button which says Create PlayMode Test Assembly Folder. You will see a new folder appear just under the RW folder. The default name Tests is fine, so you can press Enter to finalize the name.

create tests folder

You might be curious what the two different tabs inside the Test Runner are.

The PlayMode tab is for tests that will run while in Play mode (as if you were playing the game in real time). The EditMode tests will run outside of Play mode, which is great for testing things like custom Inspector behaviors.

For this tutorial, you’ll focus on PlayMode tests. But feel free to experiment with EditMode testing once you feel ready. Make sure the PlayMode tab is selected from now on when dealing with the Test Runner window.

What’s in a Test Suite?

As you learned above, a unit test is a function that tests the behavior of a small, specific, set of code. Since a unit test is a method, it needs to be in a class file in order to run.

The Test Runner will go through all your test class files and run the unit tests in them. A class file that holds unit tests is called a test suite.

A test suite is where you logically divide your tests. You want to divide your test code among different, logical suites (e.g. a test suite for physics and a separate one for combat). For this tutorial, you only need one test suite, so it’s about time you created it. :]

Setting Up the Test Assembly and the Test Suite

Select the Tests folder and in the Test Runner window, click the Create Test Script in current folder button. Name the new file “TestSuite”.

unittest create testsuite

In addition to creating the new C# file, Unity also creates another file called Tests.asmdef. This is an assembly definition file and it’s used to point Unity to where the test file dependencies are. This is because your production code is kept separate from your test code.

If you run into a situation where Unity can’t find your test files or tests, double check to make sure there’s an assembly definition file that includes your test suite. The next step is setting this up.

To ensure the test code has access to the game classes, you’ll create an assembly of your class code and set the reference in the Tests assembly. Click the Scripts folder to select it. Right-click this folder and choose Create ▸ Assembly Definition.

Name the file “GameAssembly”.

unittest create assembly file

Click the Tests folder, and then click the Tests assembly definition file. In the Inspector, click the plus button under the Assembly Definition References heading.

You will see a Missing Reference field. Click the dot next to this field to open the selector window. Select the GameAssembly file.

You should now see the GameAssembly assembly file in the references section. Click the Apply button to save these changes.

If you didn’t follow these steps, you wouldn’t have been able to reference the game class files inside the unit test files. With that out of the way, it’s finally time to code. :]

Writing Your First Unit Test

Double-click the TestSuite script to open it in a code editor. Replace all the code with the following:

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

public class TestSuite
{


}

What tests should you write? Truth be told, even in this tiny little Crashteroids game, there are a quite a lot of tests you could write to make sure everything works as expected. For this tutorial, you will only concern yourself with a few key areas around hit detection and core game mechanics.

Note: When it comes time for you to write unit tests on a production level product, it’s really worth taking the time to consider all the possible edge cases you need to test for all areas of your code.

For the first test, it would be a good idea to make sure that the asteroids actually move down. It would be really hard for the asteroids to hit the ship if they’re moving away from it! Add the following method and private variable to the TestSuite script and save:

private Game game;

// 1
[UnityTest]
public IEnumerator AsteroidsMoveDown()
{
    // 2
    GameObject gameGameObject = 
        MonoBehaviour.Instantiate(Resources.Load<GameObject>("Prefabs/Game"));
    game = gameGameObject.GetComponent<Game>();
    // 3
    GameObject asteroid = game.GetSpawner().SpawnAsteroid();
    // 4
    float initialYPos = asteroid.transform.position.y;
    // 5
    yield return new WaitForSeconds(0.1f);
    // 6
    Assert.Less(asteroid.transform.position.y, initialYPos);
    // 7
    Object.Destroy(game.gameObject);
}

There’s only a few lines of code here, but there’s a lot going on. So, take a moment and make sure you understand each part:

  1. This is an attribute. Attributes define special compiler behaviors. It tells the Unity compiler that this is a unit test. This will make it appear in the Test Runner when you run your tests.
  2. Creates an instance of the Game. Everything is nested under the game, so when you create this, everything you need to test is here. In a production environment, you will likely not have everything living under a single prefab. So, you’ll need to take care to recreate all the objects needed in the scene.
  3. Here you are creating an asteroid so you can keep track of whether it moves. The SpawnAsteroid method returns an instance of a created asteroid. The Asteroid component has a Move method on it (feel free to look at the Asteroid script under RW / Scripts if you’re curious how the movement works).
  4. Keeping track of the initial position is required for the assertion where you verify if the asteroid has moved down.
  5. All Unity unit tests are coroutines, so you need to add a yield return. You’re also adding a time-step of 0.1 seconds to simulate the passage of time that the asteroid should be moving down. If you don’t need to simulate a time-step, you can return a null.
  6. This is the assertion step where you are asserting that the position of the asteroid is less than the initial position (which means it moved down). Understanding assertions is a key part of unit testing, and NUnit provides different assertion methods. Passing or failing the test is determined by this line.
  7. Your mom might not yell at you for leaving a mess after your unit tests are finished, but your other tests might decide to fail because of it. :[ It’s always critical that you clean up (delete or reset) your code after a unit test so that when the next test runs there are no artifacts that can affect that test. Deleting the game object is all you have left to do, since for each test you’re creating a whole new game instance for the next test.

Passing Tests

Great job! You’ve written your first unit test, but how do you know if it works? The Test Runner of course! In the Test Runner window, expand all the arrows. You should see your AsteroidsMoveDown test in the list with a gray circle:

The gray circle means the test has not yet been run. When a test is run and passes it’ll show a green arrow. If a test fails, it’ll show a red X. Run the test by clicking the RunAll button.

This will create a temporary scene and run the test. When it’s done you should see that the test passed.

You have successfully created your first passing unit test, which asserts that spawned asteroids move down.

Note: Before you write unit tests of your own, you need to understand the implementation you’re testing. If you’re curious how the logic you’re testing works, feel free to look at the code under RW / Scripts.

Using Integration Tests

Before going further down the unit test rabbit hole, now is a good time to explain what integration tests are and how they’re different than unit testing.

Integration tests are tests that validate how “modules” of code work together. “Module” is another vague term, but the important distinction is that integration tests are designed to test how your software works in actual production (i.e. when a user is actually playing your game).

Say you’ve made a combat game where the player kills monsters. You might want to make an integration test to make sure that when a player kills 100 units, an achievement unlocks.

This test would span several modules of your code. It would likely involve the physics engine (for hit detection), the unit managers (which track unit health and process damage, and pass on other related events), and the event tracker which keeps track of all events fired (like “Monster Killed”). It would then call the achievement manager when it’s time to unlock an achievement.

An integration test would simulate the player killing 100 monsters and make sure the achievement unlocked. This is very different from a unit test, because it’s testing that large components of code are working together.

You won’t be exploring integration tests in this tutorial, but this should clear up the difference between what a “unit” of work is (and why it’s unit tested) vs a “module” of code (and why it’s integration tested).

Adding Tests to the Test Suite

The next test will test game over when the ship crashes into an asteroid. With the TestSuite open in the code editor, add the following test below the first unit test and save:

[UnityTest]
public IEnumerator GameOverOccursOnAsteroidCollision()
{
    GameObject gameGameObject = 
       MonoBehaviour.Instantiate(Resources.Load<GameObject>("Prefabs/Game"));
    Game game = gameGameObject.GetComponent<Game>();
    GameObject asteroid = game.GetSpawner().SpawnAsteroid();
    //1
    asteroid.transform.position = game.GetShip().transform.position;
    //2
    yield return new WaitForSeconds(0.1f);

    //3
    Assert.True(game.isGameOver);

    Object.Destroy(game.gameObject);
}

You’ve seen most of this code in the last test, but there are a few different things here:

  1. You are forcing an asteroid and ship crash by explicitly setting the asteroid to have the same position as the ship. This will force their hitboxes to collide and cause game over. If you’re curious how that code works, look at the Ship, Game, and Asteroid files in the Scripts folder.
  2. A time-step is needed to ensure the Physics engine Collision event fires so a 0.1 second wait is returned.
  3. This is a truth assertion, and it checks that the gameOver flag in the Game script has been set to true. The game code works with this flag being set to true when the ship is destroyed, so you’re testing to make sure this is set to true after the ship has been destroyed.

Go back to the Test Runner window and you will now see this new unit test list there.

This time, you’ll only run this one test instead of the whole test suite. Click GameOverOccursOnAsteroidCollision, then click the Run Selected button.

And voila, yet another test has passed.

Setting Up and Tearing Down Phases

You might have noticed there’s some repeated code between the two tests where the Game’s GameObject is created and a reference to where the Game script is set:

GameObject gameGameObject = MonoBehaviour.Instantiate(Resources.Load<GameObject>("Prefabs/Game"));
game = gameGameObject.GetComponent<Game>();

You’ll also notice it when the Game’s GameObject is destroyed:

Object.Destroy(game.gameObject);

This is very common in testing. There are actually two phases when it comes to running a unit test. The Setup phase and the Tear Down phase.

Any code inside of a Setup method will run before a unit test (in that suite), and any code in the Tear Down method will run after a unit test (in that suite).

It’s time to make life easier by moving this setup and tear down code into special methods. Open the code editor and add the following code to the top of the TestSuite file, just above the first [UnityTest] attribute:

[SetUp]
public void Setup()
{
    GameObject gameGameObject = 
        MonoBehaviour.Instantiate(Resources.Load<GameObject>("Prefabs/Game"));
    game = gameGameObject.GetComponent<Game>();
}

The SetUp attribute specifies that this method is called before each test is run.

Next, add the following method and save:

[TearDown]
public void Teardown()
{
    Object.Destroy(game.gameObject);
}

The TearDown attribute specifies that this method is called after each test is run.

With the setup and tear down code prepared, remove the lines of code that appear in these methods and replace them with the corresponding method calls. Your code will look like this afterwards:

public class TestSuite
{
    private Game game;

    [SetUp]
    public void Setup()
    {
        GameObject gameGameObject = 
            MonoBehaviour.Instantiate(Resources.Load<GameObject>("Prefabs/Game"));
        game = gameGameObject.GetComponent<Game>();
    }

    [TearDown]
    public void Teardown()
    {
        Object.Destroy(game.gameObject);
    }

    [UnityTest]
    public IEnumerator AsteroidsMoveDown()
    {
        GameObject asteroid = game.GetSpawner().SpawnAsteroid();
        float initialYPos = asteroid.transform.position.y;
        yield return new WaitForSeconds(0.1f);
  
        Assert.Less(asteroid.transform.position.y, initialYPos);
    }

    [UnityTest]
    public IEnumerator GameOverOccursOnAsteroidCollision()
    {
        GameObject asteroid = game.GetSpawner().SpawnAsteroid();
        asteroid.transform.position = game.GetShip().transform.position;
        yield return new WaitForSeconds(0.1f);

        Assert.True(game.isGameOver);
    }
}

Testing Game Over and Laser Fire

With the setup and tear down methods ready to make life easier, it’s the perfect time to add some more tests using them. The next test should verify that when the player clicks New Game that the gameOver bool is not true. Add the following test to the bottom of the file and save:

[UnityTest]
public IEnumerator NewGameRestartsGame()
{
    //1
    game.isGameOver = true;
    game.NewGame();
    //2
    Assert.False(game.isGameOver);
    yield return null;
}

This should start to look familiar, but here are a few things to take note of:

  1. This part of the code sets up this test to have the gameOver bool set to true. When the NewGame method is called, it should set this flag back to false.
  2. Here, you assert that the isGameOver bool is false, which should be the case after a new game is called.

Go back to the Test Runner, and you should see that the new test NewGameRestartsGame is there. Run that test as you’ve done before and see that it passes:

Asserting Laser Movement

The next test you add will test that the laser the ship fires moves up (similar to the first unit test you wrote). Open the TestSuite file in the editor. Add the following method and then save:

[UnityTest]
public IEnumerator LaserMovesUp()
{
      // 1
      GameObject laser = game.GetShip().SpawnLaser();
      // 2
      float initialYPos = laser.transform.position.y;
      yield return new WaitForSeconds(0.1f);
      // 3
      Assert.Greater(laser.transform.position.y, initialYPos);
}

Here’s what this code does:

  1. This gets a reference to a created laser spawned from the ship.
  2. The initial position is recored so you can verify that it’s moving up.
  3. This assertion is just like the one in the AsteroidsMoveDown unit test, only now you’re asserting that the value is greater (indicating that the laser is moving up).

Save and go back to the Test Runner. Run the LaserMovesUp test and see that it passes:

By now you should really be getting the hang of things, so it’s time to add the last two tests and finish off this tutorial. :]

Ensuring Lasers Destroy Asteroids

Next, you are going to make sure that a laser will destroy an asteroid if it hits it. Open the editor and add the following test at the bottom of TestSuite and save:

[UnityTest]
public IEnumerator LaserDestroysAsteroid()
{
    // 1
    GameObject asteroid = game.GetSpawner().SpawnAsteroid();
    asteroid.transform.position = Vector3.zero;
    GameObject laser = game.GetShip().SpawnLaser();
    laser.transform.position = Vector3.zero;
    yield return new WaitForSeconds(0.1f);
    // 2
    UnityEngine.Assertions.Assert.IsNull(asteroid);
}

Here’s how this works:

  1. You are creating an asteroid and a laser, and making sure they have the same position so as to trigger a collision.
  2. A special test with an important distinction. Notice how you are explicitly using UnityEngine.Assertions for this test? That’s because Unity has a special Null class which is different from a “normal” Null class. The NUnit framework assertion Assert.IsNull() will not work for Unity null checks. When checking for nulls in Unity, you must explicitly use the UnityEngine.Assertions.Assert, not the NUnit Assert.

Return to the Test Runner and run this new test. You’ll see that satisfying green check mark. :]

To Test or Not To Test

Deciding to commit to unit tests is a big commitment, and should not be taken lightly. However the payoffs can certainly be worth it. There is even a methodology of development known as Test Driven Development (TDD).

With TDD, you actually write tests before you write your application logic. You make tests first, ensure they fail, and then only write code designed to get the test to pass. This can be a very different approach to coding, but it also ensures you’ve written your code in a testable way.

Keep this in mind if you decide to take the plunge on your next project. For now, it’s time to write your own unit tests, but to do that you need a game — which is all provided for you.

Note: Deciding whether to test only public methods or also private methods is something you need to consider. Some people believe that private methods should only be tested through the public methods that use them. This can make the “unit” of code you need to test quite large, and might not be desirable. On the flip side, testing private methods can be problematic and requires special frameworks or using reflection tools to check things. Each scenario has its pros and cons, which are beyond the scope of this tutorial. This tutorial will set all methods to be tested to public to make things easier to follow — so don’t use this tutorial as a best practices reference when it comes to production code.

Testing can be a big commitment, so it would be worth taking a look at the pros and cons of adding unit testing to your project:

Unit Testing Pros

There are a lot of important upsides to unit testing, which include the following:

  • Provides confidence that a method behaves as expected.
  • Serves as documentation for new people learning the code base (unit tests make for great teaching).
  • Forces you to write code in a testable way.
  • Helps you isolate bugs faster and fix them quicker.
  • Prevents future updates from adding new bugs to old working code (known as regression bugs).

Unit Testing Cons

However, you might not have the time or budget to take on unit testing. These are the downsides you should consider:

  • Writing tests can take longer than writing the code itself.
  • Bad or inaccurate tests create false confidence.
  • Requires more knowledge to implement correctly.
  • Important parts of the code base might not be easily testable.
  • Some frameworks don’t easily allow private method testing, which can make unit testing harder.
  • If tests are too fragile (fail too easily for the wrong reasons), maintenance can take a lot of time.
  • Unit tests don’t catch integration errors.
  • UI is hard to test.
  • Inexperienced developers might waste time testing the wrong things.
  • Sometimes, testing things with external or runtime dependencies can be very hard.

Testing that Destroying Asteroids Raises the Score

Time to write the last test. With the code editor open, add the following to the bottom of the TestSuite file and save:

[UnityTest]
public IEnumerator DestroyedAsteroidRaisesScore()
{
    // 1
    GameObject asteroid = game.GetSpawner().SpawnAsteroid();
    asteroid.transform.position = Vector3.zero;
    GameObject laser = game.GetShip().SpawnLaser();
    laser.transform.position = Vector3.zero;
    yield return new WaitForSeconds(0.1f);
    // 2
    Assert.AreEqual(game.score, 1);
}

This is an important test that makes sure that when the player destroys an asteroid that the score goes up. Here’s how it breaks down:

  1. You’re spawning an asteroid and a laser, and making sure they’re in the same position. This ensures a collision occurs, which will trigger a score increase.
  2. This asserts that the game.score int is now 1 (instead of the 0 that it starts at).

Save your code and go back to the Test Runner to run this last test and see that it passes:

Amazing work! All the tests are passing.

Where to Go From Here?

You covered a lot of ground here. If you want to compare your work with the final project, you can find the Download Materials at the top or bottom of this tutorial.

In this tutorial, you’ve learned what unit tests are and how to write them in Unity. You also wrote six unit tests that all passed successfully and learned some of the pros and cons of unit testing.

Feeling confident? There are plenty more tests you could write. Try looking at all the game class files and writing unit tests for other parts of the code. Consider adding tests for the following scenarios:

  • Each asteroid type triggers game over when it crashes into the ship.
  • Starting a new game sets the score to 0.
  • Moving left and right works correctly for the ship.

If you’re interested in taking your unit testing to the next level, you should look into dependency injection and mocking frameworks. This can make it a lot easier to configure your tests.

Also read through the NUnit documentation to learn more about the NUnit framework.

And don’t be shy, share your thoughts and ask your questions in our forums.

Happy Testing!

Average Rating

5/5

Add a rating for this content

15 ratings

Contributors

Comments