Are you curious how unit testing works in Unity? Don't know what unit testing is in general? If you answered positively to these questions, then this tutorial will be useful to you. From it you will learn about unit testing the following:
- What it is
- Its benefits
- Advantages and disadvantages
- How it works in Unity when using Test Runner
- How to write and run unit tests that will be tested
Note : this tutorial assumes that you are familiar with C # and the basics of developing in Unity. If you are new to Unity, then first study the other tutorials on this engine .
What is a unit test?
Before delving into the code, it is important to get a clear understanding of what unit testing is. Simply put, unit testing is testing ... units.
The unit test (ideally) is intended for testing a separate unit of code. The composition of a “unit” may vary, but it is important to remember that unit testing must test exactly one “element” at a time.
Unit tests need to be created to verify that a small logical fragment of code in a particular script runs exactly as you expect. It can be difficult to understand before you start writing your own unit tests, so let's look at an example:
')
You have written a method that allows the user to enter a name. The method is written in such a way that numbers are not allowed in the name, and the name itself may consist of only ten or less characters. Your method intercepts each keystroke and adds the corresponding character in the
name
field:
public string name = "" public void UpdateNameWithCharacter(char: character) {
What's going on here:
- If the character is not a letter, the code pre-exits the function and does not add the character to the string.
- If the name is ten or more characters long, the code does not allow the user to add another character.
- If these two checks are passed, the code adds a character to the end of the name.
This unit can be tested because it is a “module” of the work being done. Unit tests
enforce the logic of the method.
Example of unit tests
How do we write unit tests for the
UpdateNameWithCharacter
method?
Before we begin to implement these unit tests, you need to carefully consider what these tests do and come up with names for them.
Look at the examples of unit test names shown below. From the titles it should be clear that they check:
UpdateNameDoesntAllowCharacterAddingToNameIfNameIsTenOrMoreCharactersInLength
UpdateNameAllowsLettersToBeAddedToName
UpdateNameDoesntAllowNonLettersToBeAddedToName
From these test method names, we can see that we are really checking whether the unit is working with the
UpdateNameWithCharacter
method. These test names may seem too long and detailed, but this is to our advantage.
Each unit test you write is part of a test suite.
The complex of tests contains all unit tests related to the logical group of the functional (for example, “unit tests of combat”). If any test in the kit fails the test, then the entire test suite fails.
Running game
Open the
Crashteroids Starter project (you can download it
from here ), and then open the
Game scene from the
Assets / RW / Scenes folder.
Click on
Play to launch Crashteroids, and then click on the
Start Game button. Move the spacecraft with the
left and
right arrows on the keyboard.
To shoot a laser beam, press the
spacebar . If the beam hits an asteroid, the score will increase by one. If the asteroid collides with the ship, the ship explodes and the game ends (with the ability to start over).
Try to play a little and make sure that after the asteroid collides with the ship, the inscription Game Over appears.
Getting started with Unity Test Runner
Now that we know how the game is executed, it is time to write unit tests to check that everything works as it should. Thus, if you (or someone else) decide to update the game, you will be sure that the update will not break anything that worked before.
To write tests, you first need to learn about Unity Test Runner.
Test Runner allows you to perform tests and check whether they pass successfully. To open the Unity Test Runner, select
Window â–¸ General â–¸ Test Runner .
After Test Runner opens in a new window, you can simplify your life by clicking on the Test Runner window and
dragging it into place next to the Scene window.
Preparing NUnit and Test Folders
Test Runner is a unit-testing function provided by Unity, but it uses the
NUnit framework. When you start working with unit tests more seriously, I recommend learning the
NUnit wiki to learn more. About everything you need for the first time will be discussed in this article.
To run the tests, we first need to create a test folder in which the test classes will be stored.
In the
Project window
, select the
RW folder. Look at the
Test Runner window and make sure
PlayMode is selected.
Click the button called
Create PlayMode Test Assembly Folder . You will see a new folder appear in the RW folder. We are satisfied with the standard name
Tests , so you can just press
Enter .
You may be wondering what these two different tabs are inside the Test Runner.
The
PlayMode tab
is used for tests performed in Play mode (when the game is running in real time). The tests on the
EditMode tab
are performed outside of Play mode, which is useful for testing things like the user behaviors in the Inspector.
In this tutorial we will look at PlayMode tests. But when you get comfortable, you can try experimenting with testing in EditMode.
When working with Test Runner in this tutorial, always check that the PlayMode tab is selected .
What is in the test suite?
As we learned above, a unit test is a function that tests the behavior of a small specific code fragment. Since a unit test is a method, it must be in a class file to run it.
Test Runner bypasses all test class files and runs unit tests from them. A class file containing unit tests is called a test suite.
In the test suite, we logically subdivide our tests. We must divide the test code into separate logical sets (for example, a test suite for physics and a separate battle kit). In this tutorial, we need only one set of tests, and it is time to create it.
Preparation of the test assembly and test suite
Select the
Tests folder and in the
Test Runner window click on the
Create Test Script in current folder button. Name the new file
TestSuite .
In addition to the new C # file, the Unity engine also creates another file called
Tests.asmdef . This is the
assembly definition file , which is used to show Unity where the dependencies of the test file are located. This is necessary because the code of the finished application is contained separately from the test code.
If you have a situation where Unity cannot find test files or tests, then make sure that there is an assembly definition file in which your test suite is included. The next step is to configure it.
In order for the test code to have access to the classes of the game, we will create an assembly of the code of the classes and specify a link in the assembly Tests. Click on the
Scripts folder to select it. Right-click on this folder and select
Create Assembly Definition .
Name the file
GameAssembly .
Click on the
Tests folder, and then on the
Tests assembly definition file. In the
Inspector, click on the
plus button under the heading
Assembly Definition References .
You will see the
Missing Reference field. Click the
dot next to this field to open a selection window. Select the
GameAssembly file.
You should see the GameAssembly assembly file in the links section. Click the
Apply button to save these changes.
If you do not follow these steps, you will not be able to reference the game class files inside the unit test files. Having dealt with this, you can proceed to the code.
We write the first unit test
Double-click the
TestSuite script to open it in the code editor. Replace all the code with this:
using UnityEngine; using UnityEngine.TestTools; using NUnit.Framework; using System.Collections; public class TestSuite { }
What tests do we need to write? Honestly, even in such a tiny game like the Crashteroids, you can write quite a few tests to check that everything works as it should. In this tutorial, we will limit ourselves to only key areas: collision recognition and basic game mechanics.
Note : when it comes to writing a production level product test, it’s worth taking enough time to consider all the boundary cases that need to be tested in all areas of the code.
As a first test, it would be nice to check if the asteroids really are moving down. It will be difficult for them to encounter a ship if they move away from it! Add the following method and private variable to the
TestSuite script:
private Game game;
There are only a few lines of code, but they do a lot of things. So let's stop and deal with each part:
- This is an attribute . Attributes define specific compiler behaviors. This attribute tells the Unity compiler that the code is a unit test. Because of this, it will appear in Test Runner when running tests.
- Create an instance of Game. Everything else is embedded in the game, so when we create it, it will contain everything that needs to be tested. In a production environment, most likely all the elements will not be inside the same prefab. Therefore, you will need to recreate all the objects needed in the scene.
- Here we create an asteroid so that we can monitor whether it is moving. The
SpawnAsteroid
method returns an instance of the created asteroid. The Asteroid component has a Move
method (if you are curious about how movement works, you can take a look at the Asteroid script inside RW / Scripts ). - Tracking the original position is necessary to ensure that the asteroid is shifted down.
- All Unity unit tests are Corutin, so you need to add a soft return. We also add a time step of 0.1 seconds to simulate the passage of time over which the asteroid should have moved down. If you do not need to simulate a time step, you can return null.
- This is the stage of approval (assertion) , in which we assert that the position of the asteroid is less than the initial position (that is, it moved down). Understanding assertions is an important part of unit testing, and NUnit provides various assertion methods. Passing or failing a test is determined by this line.
- Of course, no one curses you for the mess left after the tests are completed, but other tests may fail because of it. It is always important to clean up (delete or reset) the code after a unit test so that when you start the next unit test there are no artifacts that could affect this test. We just need to delete the game object, because for each test we create a completely new game instance.
Passing tests
Great, you wrote your first unit test, but how do you know if it works? Of course, with the help of Test Runner! In the Test Runner window, expand all the lines with arrows. You should see the
AsteroidsMoveDown
test in the list with gray circles:
A gray circle indicates that the test has not yet been performed. If the test was started and passed, then a green arrow is displayed next to it. If the test was completed with an error, then a red X will be displayed next to it. Run the test by pressing the
RunAll button.
This will create a temporary scene and run the test. Upon completion, you should see the test pass.
You have successfully written the first unit test, stating that the asteroids created are moving down.
Note : before you start writing your own unit tests, you need to understand the implementation you are testing. If you are curious how the logic you are testing works, then examine the code in the RW / Scripts folder.
Using integration tests
Before moving deeper into the rabbit hole of unit tests, it's time to tell you what integration tests are and how they differ from unit testing.
Integration tests are tests that check how code “modules” work together. “Module” is another fuzzy term. An important difference is that the integration tests should test the operation of the software in this production (that is, when the player really plays the game).
Let's say you made a fighting game where a player kills monsters. You can create an integration test to make sure that when a player kills 100 enemies, an achievement is opened (“achivka”).
This test will affect several code modules. Most likely, it will be related to the physics engine (collision detection), enemy dispatchers (tracking the health of the enemy and handling damage, as well as moving to other related events) and the event tracker, tracking all triggered events (for example, “monster killed”). Then, when it is time to unlock the achievement, he can call the achievement manager.
The integration test will simulate a player who kills 100 monsters and see if an achievement is unlocked. It is very different from the unit test, because it tests large components of the code that work together.
In this tutorial, we will not study integration tests, but this should show the difference between the “unit” of the work (and why it is being unit-tested) and the “module” of the code (and why it is being tested integrationally).
Adding test to test suite
The next test will test the end of the game when the ship collides with an asteroid. After opening
TestSuite in the code
editor , add the test shown below under the first unit test and save the file:
[UnityTest] public IEnumerator GameOverOccursOnAsteroidCollision() { GameObject gameGameObject = MonoBehaviour.Instantiate(Resources.Load<GameObject>("Prefabs/Game")); Game game = gameGameObject.GetComponent<Game>(); GameObject asteroid = game.GetSpawner().SpawnAsteroid();
We have already seen most of this code in the previous test, but there are some differences:
- We are forcing the asteroid and the ship to collide, clearly setting the asteroid with the same position as the ship. This will create a collision of their hitboxes and lead to the end of the game. If you're curious how this code works, then take a look at the Ship , Game , and Asteroid files in the Scripts folder.
- The time step is needed for the physics engine's Collision event to fire, so a delay of 0.1 seconds is returned.
- This is a statement of truth, and it checks that the
gameOver
flag in the Game script is true. The flag takes the value true while the game is running, when the ship is destroyed, that is, we test to make sure that it is set to true after the ship is destroyed.
Return to the Test Runner window and you will see that a new unit test has appeared there.
This time we will run only this one instead of the entire test suite. Click on
GameOverOccursOnAsteroidCollision , and then on the
Run Selected button.
And voila, we passed another test.
Stages of customization and destruction
You may have noticed that in our two tests there is a repeating code: where the Game object is created and where the link to the Game script is specified:
GameObject gameGameObject = MonoBehaviour.Instantiate(Resources.Load<GameObject>("Prefabs/Game")); game = gameGameObject.GetComponent<Game>();
You will also notice that the replay is in the destruction of the Game game object:
Object.Destroy(game.gameObject);
When testing this happens very often. When it comes to running unit tests, there are actually two stages: the
“setup” (Setup) phase and the
“Tear Down” phase.
All code inside the Setup method will be executed before the unit test (in this bundle), and all code inside the Tear Down method will be executed after the unit test (in this bundle).
It is time to simplify our lives by moving the setup and tear down code into special methods. Open the code editor and add the following code to the beginning of the
TestSuite file, right before the first [UnityTest] attribute:
[SetUp] public void Setup() { GameObject gameGameObject = MonoBehaviour.Instantiate(Resources.Load<GameObject>("Prefabs/Game")); game = gameGameObject.GetComponent<Game>(); }
The
SetUp
attribute indicates that this method is called before each test
SetUp
.
Then add the following method and save the file:
[TearDown] public void Teardown() { Object.Destroy(game.gameObject); }
The
TearDown
attribute indicates that this method is called after the execution of each test.
Having prepared the code for tuning and destruction, we will remove the lines of code present in these methods and replace them with calls to the corresponding methods. After that the code will look like this:
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); } }
Test Game Over and Laser Shooting
Having prepared the methods of adjustment and destruction that simplify our life, we can proceed to adding new tests in which they are used. The following test should verify that when a player clicks the
New Game , the
gameOver bool value is not true. Add such a test to the end of the file and save it:
[UnityTest] public IEnumerator NewGameRestartsGame() {
This should seem familiar to you, but it is worth mentioning the following:
- This piece of code prepares this test to ensure that the boolean
gameOver
flag must be true. When calling the NewGame
method, it must set the flag to false
again. - Here we claim that bool
isGameOver
is false
, which should be true when calling a new game.
Go back to the Test Runner and you should see that there is a new test
NewGameRestartsGame . Run this test, as we did before, and you will see that it is successfully executed:
The statement about the movement of the laser beam
The next test you need to add is a test of the laser beam being fired by the ship flying upwards (similar to the first unit test we wrote). Open the
TestSuite file in the editor. Add the following method and save the file:
[UnityTest] public IEnumerator LaserMovesUp() {
This is what this code does:
- Receives a link to the created laser beam emitted from the ship.
- The starting position is recorded so that we can check that it is moving up.
- This statement corresponds to the statement from the unit test
AsteroidsMoveDown
, only now we claim that the value is greater (that is, the laser moves up).
Save the file and return to Test Runner. Run the LaserMovesUp test and watch it pass:Now you should begin to understand how everything works, so it’s time to add the last two tests and complete the tutorial.Check that the laser destroys asteroids
Further we will be convinced that at hit the laser destroys an asteroid. Open the editor and add the following test to the end of TestSuite , and then save the file: [UnityTest] public IEnumerator LaserDestroysAsteroid() {
Here's how it works:
- We create an asteroid and a laser beam, and assign them the same position to trigger a collision.
- This is a special test with an important difference. See that we explicitly use UnityEngine.Assertions for this test ? This is because Unity has a special Null class that is different from the “regular” Null class. NUnit framework approval
Assert.IsNull()
will not work in Unity checks to null. When checking for null in Unity, you need to explicitly use UnityEngine.Assertions.Assert, and not Assert from NUnit.
Return to the Test Runner and launch a new test. You will see a green icon pleasing us.Test or not test - that is the question
Deciding to stick with unit tests is not an easy decision, and should not be taken lightly. However, the benefits of tests are worth the effort. There is even a development methodology, called test-driven development (Test Driven Development, TDD).Working as part of TDD, you write tests before writing the application logic itself. First, you create tests, make sure that the program does not pass them, and then write only the code intended for passing the tests. It’s a very different approach to coding, but it guarantees that you are writing code in a testable manner.Keep this in mind when you start working on the next project. But for now, it's time to write your own unit tests, for which you need a game, which we have given you.: — , . , . «» , , . , . , . , , .
Testing can be a great investment, so it’s worth considering the advantages and disadvantages of adding unit testing to your project:Advantages of unit testing
Unit testing has many important advantages, including the following:- It gives confidence that the method behaves as expected.
- It serves as documentation for new people studying the code base (unit tests are great for teaching).
- Makes you write code in a testable way.
- Allows you to isolate and fix errors faster.
- Does not allow future updates to add new bugs to the old working code (they are called regression errors).
Lack of unit testing
However, you may not have the time or budget for unit testing. Here are its shortcomings that need to be taken into account:- Writing tests may take longer than the code itself.
- .
- .
- , .
- , -.
- ( ), .
- - .
- UI .
- .
- .
,
It is time to write the last test. Open the code editor, add the code shown below to the end of the TestSuite file and save it: [UnityTest] public IEnumerator DestroyedAsteroidRaisesScore() {
This is an important test, verifying that when a player destroys an asteroid, the score increases. This is what it consists of:- We create an asteroid and a laser beam, and put them in one position. Due to this, there is a collision that triggers an increase in the account.
- The statement that game.score is now equal to 1 (and not 0, as it was at the beginning).
Save the code and go back to Test Runner to run this last test and see if the game is running:Awesome! All tests passed.Where to go next?
In the article we reviewed a large amount of information. If you want to compare your work with the final project, then look at it in the archive , the link to which is also indicated at the beginning of the article.From this tutorial, you learned what unit tests are and how to write them in Unity. In addition, you wrote six unit tests that successfully passed the code, and met some of the advantages and disadvantages of unit testing.Feeling confident? Then you can write many more tests. Examine the game class files and try to write unit tests for other parts of the code. Consider adding tests for the following scenarios:- Each type of asteroid at the touch of a ship leads to the end of the game.
- Running a new game clears the score.
- Movement left and right for the ship is working properly.
If you want to increase your level of knowledge about unit testing, then it is worth exploring dependency injection and frameworks for working with mock objects . This can greatly simplify test setup.Also read the NUnit documentation to learn more about the NUnit framework.And feel free to share your thoughts and questions on the forums.Successful testing!