Introduction To Unity Unit Testing

[ad_1]

Testing is a part of game development that’s often not given enough attention — especially in smaller studios without the resources for a dedicated testing team. Fortunately, as a developer, you can proactively use unit tests to help produce a top-quality project. In this tutorial, you’ll learn the following about unit testing in Unity:

  • What is a unit test?
  • What value do unit tests offer?
  • Pros and cons to writing unit tests.
  • How to import and use the Unity Test Framework.
  • Writing and running unit tests that pass.
  • Using Code Coverage reports to analyze how thorough your test cases are.

What is a Unit Test?

Before diving into code, it’s important to have a solid understanding of what unit testing is.

A unit test tests 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 test one thing at a time. Usually, this is a single method in your game logic.

You should design a unit test to validate that a small, logical snippet of code performs 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 wrote 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 must be 10 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;
}

Here’s what’s going on in the code above:

  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 10 characters or more, it prevents the user from adding another character.
  3. Once those two cases 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 implementing these unit tests, think carefully about what the tests do and name them accordingly.

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

  • UpdateNameDoesntAllowCharacterIfNameIsTenOrMoreCharactersInLength
  • UpdateNameAllowsLettersToBeAddedToName
  • UpdateNameDoesntAllowNonLettersToBeAddedToName

From these test method names, you can see what you’re testing and that the “unit” of work performed by UpdateNameWithCharacter is doing what it should. These test names might seem long and specific, but they can be helpful for you, or any other developer that comes along later, should any of the tests fail. A descriptive name will reveal the potential issue far quicker than having to dive into the code and check all the parameters around the test case.

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. If any individual test in a test suite fails, the entire test suite fails.

unit testing: test suite

Getting Started

Download the starter project by clicking the Download Materials link at the top or bottom of the tutorial. Open the Game scene in Assets / Scenes.

unit testing: game one

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

Press the space bar to fire a laser. If a laser hits an asteroid, the score will increase by one. If an asteroid hits the ship, the ship will explode and it’s game over (with the option to start again).

unit testing: game two

Play for a while, and then allow the ship to get hit by an asteroid to see that game over triggers.

unit testing: game three

Getting Started with the Unity Test Framework

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 someone else) decide to update the game, you can be confident in knowing the update didn’t break anything that was working before.

Before you write tests, make sure the Unity Test Framework is added to the project. Go to Window ▸ Package Manager and select Unity Registry from the drop-down. Scroll down the list to find the Test Framework package and install or update.

Test Framework Package

With the package installed, you can now open Unity’s Test Runner. The Test Runner lets you run tests and see if they pass. To open the Unity Test Runner, select Window ▸ General ▸ Test Runner.

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 one of your other windows to dock it.

GIF of moving Test Runner tab

Setting Up Test Folders

Unity Test Framework is the unit testing feature provided by Unity — but it utilizes the open source library NUnit. As you get more serious about writing unit tests, you should consider reading the docs 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 Assets folder. Look at the Test Runner window and make sure PlayMode is selected.

Click the button that says Create PlayMode Test Assembly Folder. You’ll see a new folder appear just under the Assets folder. The default name Tests is fine, so press Enter to finalize the name.

create tests folder

There are two different tabs inside the Test Runner:

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.

Note: 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. Divide your test code among different logical suites — like a test suite for physics and a separate one for combat. For this tutorial, you only need one test suite — and it’s time to create 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

You might notice that there was already another file in the Tests folder. Unity also created the file Tests.asmdef when you created the folder in the first step. 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 game 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.

Create Assembly Definition

Name the file GameAssembly.

unittest create assembly file

Next, you need to add a reference of the GameAssembly to the Tests assembly. To do this:

  1. Click the Tests folder, and then click the Tests assembly definition file.
  2. In the Inspector, click the plus button under the Assembly Definition References heading.
  3. Drag the GameAssembly into the new field.

Drag GameAssembly

You’ll see the GameAssembly assembly file in the references section. Click the Apply button at the bottom of the Inspector window to save these changes.

Apply button

This process lets you reference the game class files inside the unit test files — and so now you can write tests.

Writing Your First Unit Test

Double-click the TestSuite script to open it in a code editor. Before you get started, read through the template code in the file. You’ll see two methods in there: TestSuiteSimplePasses() and TestSuiteWithEnumeratorPasses. Notice they have different attributes attached to them — [Test] and [UnityTest], respectively. The comments in the template explain this, but while a [Test] case executes like a regular method, [UnityTest] acts as a Coroutine in which you can use yield statements to wait for certain things to happen. This becomes particularly useful when testing physics code, which you’ll do now!

Replace all the code in TestSuite.cs with the following:

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

public class TestSuite
{

}

What tests should you write? Truthfully, even in this tiny little Crashteroids game, there are quite a lot of tests you could write to make sure everything works as expected. For this tutorial, you’ll focus on hit detection and core game mechanics.

Note: When you write unit tests on a production-level product, it’s 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’s 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:

// 1
private Game game;

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

There’re 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 a class-level reference to the Game class. This gives you access to key game logic for testing.
  2. This is an attribute, as described above. 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.
  3. 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.
  4. 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 (check out the Asteroid script under Assets / Scripts if you’re curious how the movement works).
  5. Keeping track of the initial position is required for the assertion where you verify if the asteroid has moved down.
  6. As you’re using a UnityTest coroutine, you have to add a yield statement. In this case, you’re also adding a time-step of 0.1 seconds to simulate the passage of time that the asteroid should be moving down.
  7. 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.
  8. Be sure to clean up after yourself! It’s critical to delete or reset your code after a unit test so that when the next test runs there aren’t artifacts that could affect that test. Deleting the game object clears away anything else that might be created.

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’ll see your AsteroidsMoveDown test in the list with a gray circle:

ASteroidsMoveDown

The gray circle means the test hasn’t 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.

Run All button

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

Test run done

Congratulations! You successfully created your first passing unit test, and it verifies 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, review the code under Assets / Scripts.

Adding Tests to the Test Suite

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

[UnityTest]
public IEnumerator GameOverOccursOnAsteroidCollision()
{
    GameObject gameGameObject =
        Object.Instantiate(Resources.Load<GameObject>("Prefabs/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 saw most of this code in the last test, but there are a few different things here:

  1. You’re forcing an asteroid and ship to 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 Boolean 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’ll now see this new unit test list there.

GameOverOccursOnASteroidCollision

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

Run Selected button

And there you go — another test has passed. :]

Another run success

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 =
        Object.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);

It’s very common in testing to have this type of code — where you create the test environment and then clean it up at the end. But, it’s also good practice to keep your code DRY!

The Unity Test Framework provides two more attributes to help 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 any unit test in that suite, and any code in the Tear Down method will run after every unit test in that suite.

It’s time to move 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 =
        Object.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:

public class TestSuite
{
    private Game game;

    [SetUp]
    public void Setup()
    {
        GameObject gameGameObject = 
            Object.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, it’s the perfect time to add more tests using them. The next test will verify that when the player clicks New Game, the gameOver bool is not true. Add the following test to the bottom of the file and save:

//1
[Test]
public void NewGameRestartsGame()
{
    //2
    game.isGameOver = true;
    game.NewGame();
    //3
    Assert.False(game.isGameOver);
}

This will look familiar, but here are a few things to notice:

  1. This test won’t require any time to pass, so it uses the standard [Test] attribute, and the method type is just void.
  2. This part of the code sets up this test to have the gameOver bool set to true. When the NewGame method is called, it will set this flag back to false.
  3. 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’ll see the new test NewGameRestartsGame is there. Run that test as you’ve done before and see that it passes:

NewGameRestartsGame

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 recorded so you can verify that it’s moving up.
  3. This assertion is just like the one in the AsteroidsMoveDown unit test, except 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:

LaserMovesUp

Now you’re firing through these test cases, so it’s time to add the last two tests and finish off this tutorial. :]

Ensuring Lasers Destroy Asteroids

Next, you’re 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’re creating an asteroid and a laser, and you’re making sure they have the same position to trigger a collision.
  2. This is a special test with an important distinction. Notice how you’re explicitly using UnityEngine.Assertions for this test? That’s because Unity has a special Null class that’s 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.

LaserDestroysAsteroid

To Test or Not To Test

Committing to unit tests is a big decision, and it shouldn’t be taken lightly. However, the payoffs can certainly be worth it. There’s 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.

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 — and that’s 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 project as a best practices reference when it comes to production code!

Testing can be a big commitment, so it’s 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, including:

  • 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 also 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.
  • 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, the score goes up. Here’s how it breaks down:

  1. You’re spawning an asteroid and a laser, and you’re making sure they’re in the same position. This ensures a collision occurs, which will trigger a score increase. This code is the same as the previous test.
  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:

DestroyedAsteroidRaisesScore

Amazing work! All the tests are passing.

Code Coverage

With six different unit tests covering what is a fairly basic project, you would think that there’s pretty good coverage of the code base. Fortunately, Unity also provides another tool that will show you exactly how much of the code is covered by unit tests. What’s more, it will show you how this coverage changes over time between test runs. So as you add more code, your coverage may go down. And as you add tests, coverage should go up!

Time to take a quick look at the Code Coverage package. Open the Package Manager window once more from Window ▸ Package Manager and select Unity Registry from the drop-down. Scroll down the list to find the Code Coverage package and install it to the project.

Code Coverage

Once the package has installed, open the Code Coverage window by selecting Window ▸ Analysis ▸ Code Coverage.

Window Analysis Code Coverage

When you open this window for the first time, you may see a warning that Code Coverage requires the Unity Editor to be in Debug mode. If so, click the button to switch to debug mode.

Next, check the box to Enable Code Coverage.

Switch to debug mode, Enable Code Coverage

The rest of the settings are fine as they are, but the two important ones here are Auto Generate Report and Auto Open Report. With those two options selected you can go straight into generating your first report!

Note: This tutorial was created using Unity version 2021.3.18f1. The layout and options of the Code Coverage screen have since changed and Auto Open Report may no longer be available for your version of the editor, in which case you can ignore this.

Head back to the Test Runner window and select Run All to re-run all your tests. Once the Test Runner is complete, a window will open by default in the path that’s set in the Code Coverage window. The file index.html will be selected by default. Open this file in your default browser to view the code coverage report.

Coverage report

You can see in the report that with just six test cases, you have already covered more than 70% of the game’s code base! With a quick glance down the different scripts listed inside the GameAssembly, you can see that most have good coverage. But the Ship class could definitely use some further coverage…

Click on the Ship class in the list, and it will open a new view that shows you all the lines of code and highlights those that are and are not tested. Scroll down to the bottom of the class and you will see SpawnLaser which you called in a few of your tests. Notice it is marked green.

Ship coverage

Also near the bottom of the class are Explode and RepairShip. They’re marked green too, but you didn’t write explicit test cases for those! However, Explode is called from GameOver, which you wrote a test for, and RepairShip is called from NewGame, which you also wrote a test case for. These methods have been covered by extension of existing tests.

You can also see from your test report that there are two player movement methods that remain untested. Try writing test cases for these (use the AsteroidsMoveDown test case as a reference), and then check your coverage report again. Note that if you only run the new test, the coverage report will also only cover that test — so you want to Run All to get the full report.

Your new report should show some improved coverage for the Ship class and overall.

Improved coverage

Where to Go From Here?

Download the completed project files by clicking the Download Materials link at the top or bottom of the tutorial.

You covered a lot of ground here. In this tutorial, you 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:

  • Moving left and right works correctly for the ship.
  • Not only testing how the ship moves, but also that it stays within limits.
  • Starting a new game sets the score to 0.
  • An asteroid that moves off the bottom of the screen should be destroyed.

If you’re interested in taking your unit testing to the next level, 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.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

[ad_2]

Source link