What is unit testing + why should I do it?
The best way of thinking about unit tests is that they should be designed to test small pieces of your systems in isolation. In Unity, this might be a single script or a helper function.
Unit tests tend to be faster to write since you're focused on one specific piece rather than how individual pieces fit together.
While there's always a time and place for larger end-to-end tests, typically when building a game you'll want to prioritise creating quick tests for complicated logic - most of the time just to sanity-check it.
What is the Unity Test Framework?
The Unity Test Framework is a convenient test runner built into Unity and based off the popular NUnit framework. Unity Test Framework abstracts away the complicated logic required to build and load your game before tests, allowing you to create Play-mode tests and Edit-mode tests. In this post we'll be focusing on Play-mode tests.
It is available as a package that you can install through the Unity Package Manager.
In this post we'll go over how to use the Unity Test Framework to write good tests. You'll need to setup the test runner and assemblies as described in the Unity docs to get started.
Your first test
For this post, we'll be looking at real-world tests we created for Talo to illustrate some of these examples. When you create your first test, you'll have a simple script that looks like this:
public class MyFirstTest
{
// A UnityTest behaves like a coroutine in Play Mode. In Edit Mode you can use
// `yield return null;` to skip a frame.
[UnityTest]
public IEnumerator MyFirstTestWithEnumeratorPasses()
{
// Use the Assert class to test conditions.
// Use yield to skip a frame.
yield return null;
}
}
As you can see, Unity uses Coroutines to run tests. Coroutines are great because they allow you to pause execution and wait for actions to happen before resuming.
The [UnityTest]
attribute decorates the function to allow us to use these coroutines to skip frames or enter Play-mode.
Lets create a test for the GetProp
function from the following script:
public class LiveConfig
{
private Prop[] props;
public LiveConfig(Prop[] props)
{
this.props = props;
}
public T GetProp<T>(string key, T fallback = default(T))
{
try
{
Prop prop = props.First((prop) => prop.key == key);
return ((T)Convert.ChangeType(prop.value, typeof(T)));
}
catch (Exception)
{
return fallback;
}
}
}
This function simply uses LINQ to search an array of key/value pairs (Props
) and find the first one with a matching key. It then converts the value of that Prop
to match the generic type. If we don't find a Prop
with that key, we should return the fallback instead.
Testing the GetProp function
Appropriately, our first test should make sure that the function returns the expected result. Here's what that test would look like:
[UnityTest]
public IEnumerator GetProp_WithALiveConfigThatHasValues_ReturnsCorrectValue()
{
var config = new LiveConfig(new[] { new Prop(("gameName", "Crawle")), new Prop(("halloweenEventEnabled", "True")) });
Assert.AreEqual("Crawle", config.GetProp("gameName", "No name"));
yield return null;
}
The first thing to note is the structure of the function name. NUnit tests typically have a standardised name made of up of 1) the name of the function you're testing, 2) the condition you're testing and 3) the expected outcome.
We're testing that when the GetProp
function correctly finds the Prop
with the key we're searching for when the LiveConfig
has an array of Props
. We're doing this because the LINQ First
function throws when searching an empty array.
By using the Assert
class, we can compare any number of different outcomes. In this case we're doing a simple equality check. Always remember to yield return null
at the end of your test, to mark the coroutine as complete.
Luckily for us this function is really small and therefore easy to test. We wrote a few more tests to handle the cases for empty arrays, missing keys, etc. You can find all of those in our GitHub repo.
Handling pre-test setup
In the complicated world of game development it's pretty difficult to write totally isolated code. You'll usually have a number of dependencies that need to be in place before you can properly test what you need to test.
NUnit solves this problem by introducing the [OneTimeSetUp]
attribute that runs the method it decorates before your tests.
The code below highlights a common scenario where you need some sort of "Manager script" to be ready before executing in the scripts that it manages:
public class UpdateSaveTest
{
private TaloManager tm;
[OneTimeSetUp]
public void SetUp()
{
tm = new GameObject().AddComponent<TaloManager>();
tm.settings = ScriptableObject.CreateInstance<TaloSettings>();
}
[UnityTest]
public IEnumerator UpdateSave_InOfflineMode_UpdatesTheSaveName()
{
var api = new SavesAPI(tm);
Talo._saves = api;
api._allSaves.Add(new GameSave() { id = -1, name = "Offline Save" });
api.WriteOfflineSavesContent(new OfflineSavesContent(api._allSaves.ToArray()));
_ = api.UpdateSave(-1, "New Name");
Assert.AreEqual(1, api.GetOfflineSavesContent().saves.Length);
Assert.AreEqual("New Name", api.GetOfflineSavesContent().saves[0].name);
yield return null;
}
}
If you've used Talo (and if you haven't - you should definitely check it out), you know that it requires a settings asset. This asset is passed to the TaloManager
which passes these settings down to individual APIs.
Here, we're testing the SavesAPI
. We need the SavesAPI
to have a manager and that's what we're setting up in the SetUp
function before our test runs.
NUnit has a number of other convenient attributes like [SetUp]
that runs before each test and [TearDown]
that runs after each test to keep your test environment clean and avoid flaky tests.
Quick tips for writing unit tests
Different people have different thoughts on what makes a good unit test. Sometimes testing feels like uncharted territory in the world of game development, but here are some thoughts about how to go about it:
- Pick your battles. Some things just work so trying to test everything will inevitably just slow you down. For example, test a weird interaction between two items over a basic 2D movement script.
- Keep your tests small. In Unity, that usually means testing functions within scripts but maybe not the entire script.
- Mock as much as possible. Don't re-write code you've already written just for a test, try and find clever workarounds to prevent writing duplicate code.
Closing thoughts
Unit tests bring a level of confidence you otherwise wouldn't have in a piece of code, however they take time to create and need to be maintained going forward.
This post only scratches the surface on helpful attributes and assertions provided by NUnit and the Unity Test Framework. You can test specific platforms, move from Edit-mode to Play-more and more. Check out the Unity Test Framework docs for additional use-cases.
Testing has sometimes felt a bit abstract in game development and we've only just started the process of adding tests to the Talo Unity package which you can see in our repo.
Build your game faster with Talo
Don't reinvent the wheel. Integrate leaderboards, stats, event tracking and more in minutes.
Using Talo, you can view and manage your players directly from the dashboard. It's free!
Get started
More from the Talo Blog
Exploring Talo’s new Caddy self-hosting template
A breakdown of Talo’s latest Caddy-based self-hosting option, plus a look at other self-hosting templates available for your game.
Changelog: group updates + new Godot game save demo
All the highlights from Talo’s October 2024 releases across the dashboard, backend, Godot plugin and Unity package.
How to see live online player counts in your Godot game
Have you ever wondered how many players are current playing your Godot game? Talo makes it easy to find out using player groups.
How to load and save game state using Godot
A quick and easy example of how to use Talo Game Saves to handle saving and loading scenes in your Godot game even when players are offline.