Unity Test Framework: waiting the right way
In the previous post, we went through an introduction to the Unity Test Framework. In this tutorial, we will see the different ways to wait for something to happen during a Unity test.
Table of Contents
Understanding UnityTest Coroutines
The default Test
attribute from NUnit will run the entire test inside 1 frame, which is often limiting. To overcome that, The Unity Test Framework provides a UnityTest
attribute to run your tests as coroutines. These kinds of tests are useful because they allow you to run a test spanning more than 1 frame.
How do these coroutines work? UnityTest are essentially functions declared with a return type of IEnumerator
and containing one or more yield return
statement. The Enumerator thus represents each steps of the coroutine, and the yield return
statements are its pause points.
The most basic way to pause a coroutine is to to return a null value:
yield return null;
This will pause the coroutine until the next frame. Once the next frame is being processed, the function will continue its execution past the yield statement.
The WaitForSeconds
class
If you know the exact amount of time you need to wait before something happens (like an animation for example), you can tell the test to wait a specific amount of time thanks the the WaitForSeconds
class:
yield return new WaitForSeconds(10f);
Also, as we explained in our previous Unity Test Tip, WaitForSeconds
depends on Time.timeScale
. So be careful if your game has time shifting mechanics.
A practical example
As a practical example, we will continue using our sample project, and write a new test using the WaitForSeconds
class. (If you didn't went through the previous tutorial, now might be a good time).
Find our sample project on github and switch to the tutorial-basics
branch (this corresponds to the state of the project after completing our previous tutorial):
Once on the tutorial-basics branch, download the project by clicking Code > Download Zip.
Open the project with Unity. Open Assets/Tests/TestSuite.cs
. Add a new test to the file, and instantiate a player and a skeleton:
[UnityTest]
public IEnumerator TestSkeletonFollowsPlayer()
{
Vector3 playerPos = new Vector3(2f, 1f, -5f);
Quaternion playerDir = Quaternion.identity;
Vector3 skeletonPos = new Vector3(2f, 0f, 5f);
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;
}
Here we place the player and the skeleton far from each other. Our intent is to check if the skeleton will follow the player. Also note that we need to yield return
a value, since this is required by the IEnumerator
type. The test doesn't do anything yet so we simply return a null
value.
The Skeleton requires a player to follow, so we give it one:
GameObject player = GameObject.Instantiate(playerPrefab, playerPos, playerDir);
GameObject skeleton = GameObject.Instantiate(skeletonPrefab, skeletonPos, skeletonDir);
skeleton.GetComponent<Skeleton>().player = player.GetComponent();
yield return null;
The test is ready, now we can write the important part. We let the game run for a few seconds and verify if the skeleton moved closer to the player:
yield return new WaitForSeconds(3f);
float distance = Math.Abs((skeleton.transform.position - player.transform.position).magnitude);
Assert.That(distance, Is.LessThan(2f));
Since Math
depends on the System
namespace we need to add it on top of our file:
using System;
If you run the test in the Unity editor, you should see the skeleton follow the player, and the test pass successfully.
A smarter way to wait
As you see, WaitForSeconds
is very useful, but static wait values can be clunky. Such "magic numbers" can lead to flakiness. If the condition could be fulfilled before the static wait, we're also wasting time. If you write an extensive battery of tests (as you should), then this adds up rapidly.
We need a more explicit wait. We need to express a condition and tell the test "wait until this condition is fulfilled".
To achieve this, we will write a utility class. It takes a function as an input, and runs it every frame until it returns true:
public class Wait
{
static public IEnumerator Until(Func<bool> condition, float timeout = 30f)
{
float timePassed = 0f;
while (!condition() && timePassed < timeout) {
yield return new WaitForEndOfFrame();
timePassed += Time.deltaTime;
}
if (timePassed >= timeout) {
throw new TimeoutException("Condition was not fulfilled for " + timeout + " seconds.");
}
}
}
Note that if the condition is not fulfilled after the given time, the test will fail. This is very important, as without a timeout the method will simply run forever.
If we use this new utility class in our test, the final code looks like this:
[UnityTest]
public IEnumerator TestSkeletonFollowsPlayer()
{
Vector3 playerPos = new Vector3(2f, 1f, -5f);
Quaternion playerDir = Quaternion.identity;
Vector3 skeletonPos = new Vector3(2f, 0f, 5f);
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();
yield return Wait.Until(() => {
float distance = Math.Abs((skeleton.transform.position - player.transform.position).magnitude);
return distance <= 2f;
}, timeout: 10f);
}
Run the test and observe how it passes as soon as the condition is fulfilled. This is a more efficient test and is flexible enough to work even if the speed of the skeleton changes.
A note on WaitForEndOfFrame
In our utility class we use WaitForEndOfFrame
. It is important to remind you that this coroutine doesn't work in batchmode when used in the editor.
If you plan to run editor tests in batchmode, using a yield return null
statement is preferable. When it comes to Game Conductor however, WaitForEndOfFrame
is supported as your tests run in PlayMode.
Here are two link to learn more about this:
Conclusion
As we've seen, waiting during tests can be simple, but also achieved in a more efficient way. Writing efficient tests requires some experience, and with time anyone can acquire this skill. I hope this tutorial helped you getting closer to this goal.