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. By Anthony Uccello.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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. :]