How do you write unit tests for code that uses the Unity engine to play sounds, make web calls, or render graphics? Today’s article shows one solution!

Let’s say you have a simple class that makes web calls easier by providing an event when they’re done:

using System;
using System.Collections;
 
using UnityEngine;
 
namespace Runtime
{
	public class WebCallRunner
	{
		private WWW www;
 
		public event Action<byte[]> OnDone;
 
		public WebCallRunner(WWW www)
		{
			this.www = www;
		}
 
		public IEnumerator Run()
		{
			while (www.isDone == false)
			{
				yield return null;
			}
			if (OnDone != null)
			{
				OnDone(www.bytes);
			}
			www.Dispose();
		}
	}
}

You’d use it like this:

using UnityEngine;
 
class MyScript : MonoBehaviour
{
	void Start()
	{
		var runner = new WebCallRunner(new WWW("http://server.com/highscores"));
		runner.OnDone += HandleHighScores;
		StartCoroutine(runner.Run());
	}
 
	private void HandleHighScores(byte[] response)
	{
		// ... do something with the high scores response
	}
}

It’s really simple, not very robust, and of dubious utility, so you probably won’t find WebCallRunner useful in your projects. Nevertheless, it’s a good example of the kind of code that, with a little more robustness, you might find in a Unity app. Perhaps the production version would add some standard headers, handle errors, and parse JSON responses. In any case, the point is that it uses the Unity engine by using the WWW class.

Since WebCallRunner uses the Unity engine, we can’t unit test it. That’s because the Unity engine isn’t available to us when running the unit tests, such as from a build server or an IDE like Visual Studio or MonoDevelop. More importantly, the unit tests shouldn’t be making web calls in the first place. We don’t want them to fail when there are network issues and we don’t want them to be slowed down on slow network connections. We want fast, deterministic unit tests and that requires faking the network connection.

That leads to today’s strategy: wrap WWW in interfaces to abstract it. Here’s how that’d look:

using System;
 
public interface IWww : IDisposable
{
	bool isDone { get; }
	byte[] bytes { get; }
}
public interface IWwwFactory
{
	IWww Create(string url);
}
using UnityEngine;
 
public class UnityWww : IWww
{
	private WWW www;
 
	public UnityWww(string url)
	{
		www = new WWW(url);
	}
 
	public bool isDone { get { return www.isDone; } }
 
	public byte[] bytes { get { return www.bytes; } }
 
	public void Dispose()
	{
		www.Dispose();
	}
}
public class UnityWwwFactory : IWwwFactory
{
	public IWww Create(string url)
	{
		return new UnityWww(url);
	}
}

Interfaces can’t define a constructor, so we had to create a factory too.

Now we need to refactor the WebCallRunner to use these new interfaces instead of directly using the Unity API. All that needs to be done is to swap the WWW for IWww:

using System;
using System.Collections;
 
namespace Runtime
{
	public class WebCallRunner
	{
		private IWww www;
 
		public event Action<byte[]> OnDone;
 
		public WebCallRunner(IWww www)
		{
			this.www = www;
		}
 
		public IEnumerator Run()
		{
			while (www.isDone == false)
			{
				yield return null;
			}
			if (OnDone != null)
			{
				OnDone(www.bytes);
			}
			www.Dispose();
		}
	}
}

Next we need to change the code that uses it to use UnityWww instead of WWW:

using UnityEngine;
 
class MyScript : MonoBehaviour
{
	void Start()
	{
		var runner = new WebCallRunner(new UnityWww("http://server.com/highscores"));
		runner.OnDone += HandleHighScores;
		StartCoroutine(runner.Run());
	}
 
	private void HandleHighScores(byte[] response)
	{
		// ... do something with the high scores response
	}
}

Now that the abstraction is in place we can write a unit test using NUnit, which is built into Unity. NSubstitute, from the Unity Test Tools package on the Asset Store, is a handy way to make a fake/mock/substitute class that implements IWww. That allows us to avoid using a real UnityWww, which would make real web calls. It also allows us to skip writing a FakeWww class by hand, which would be tedious.

Here’s the unit test:

using System;
 
using NSubstitute;
using NUnit.Framework;
 
[TestFixture]
public class TestWebCallRunner
{
	[Test]
	public void RunYieldsUntilWwwIsDoneThenDispatchesEventAndDisposesWww()
	{
		var www = Substitute.For<IWww>();
		var done = false;
		www.isDone.Returns(x => done);
		www.bytes.Returns(new byte[]{ 1, 2, 3, 4, 5 });
		var webCall = new WebCallRunner(www);
		var dispatchedBytes = default(byte[]);
		webCall.OnDone += b => dispatchedBytes = b;
 
		var enumerator = webCall.Run();
 
		// not done, so keeps going
		Assert.That(enumerator.MoveNext(), Is.True);
		Assert.That(dispatchedBytes, Is.Null);
		www.DidNotReceive().Dispose();
 
		// still not done, so keeps going
		Assert.That(enumerator.MoveNext(), Is.True);
		Assert.That(dispatchedBytes, Is.Null);
		www.DidNotReceive().Dispose();
 
		done = true;
 
		// done, so enumerator stops, callback is called, IWww is disposed
		Assert.That(enumerator.MoveNext(), Is.False);
		Assert.That(dispatchedBytes, Is.EqualTo(new byte[]{ 1, 2, 3, 4, 5 }));
		www.Received(1).Dispose();
	}
}

Substitute.<IWww>() creates an instance of a class that implements IWww. Then we set it up to do what we want. We make the isDone property call a lambda that returns the local done variable and the bytes property return a hard-coded byte array. It’s now ready to be used by WebCallRunner.

The Run function returns an IEnumerator which would normally get passed to StartCoroutine. There are two problems with that when it comes to unit testing. First, that’s yet-another part of the Unity engine that we’d need to abstract using even more interfaces. Second, it happens more or less automatically and we want more control during the unit test. Thankfully, we can simulate a coroutine by calling the MoveNext function on IEnumerator directly.

So as we call MoveNext we can check that the function’s not done until our substitute/fake/mock IWww returns true, which we do by setting the local variable. We also check that the event doesn’t get dispatched and that the fake IWww doesn’t get disposed until that that point. We can check the calls to Dispose using some extension functions that NSubstitute provides. First we use www.DidNotReceive().Dispose() to assert that Dispose was not called. Then we use www.Received(1).Dispose() to assert that it was called one time.

At this point we’ve effectively abstracted the Unity engine from our logic by using interfaces. It’s a workable solution that can be done purely in C# code and allows us to test all of our logic, but not the code that actually uses the Unity engine: UnityWww and UnityWwwFactory. It’s pass-through code and missing code coverage for that part is probably just fine for most projects.

There are more important issues with this approach though. It’s a lot of boilerplate code to write every time there’s a new part of the Unity engine you want to use. It’s annoying and time-consuming to create all the files and type it all out. It’s error-prone too since you can’t unit test this code. You can easily go into a haze and start copying and pasting and get something wrong.

It also bloats the app, both in binary size and in number of files to read through. Now there’s a file for the interface and a file for the class mirroring everything you use in the Unity API. There’s probably also an interface and a class for a factory since you’ll need some way to create the type without directly calling the Unity engine’s constructor. All these extra files and types add to the noise of the project. They add a weird, parallel API wrapping the familiar Unity API.

Finally, this approach adds a virtual function call to every Unity API function call. That’s required because functions that implement interface functions have to be virtual. Virtual functions are slower than normal functions and these just turn around and make (usually) non-virtual function calls into the Unity API. The app will slow down a bit for each of these calls.

These are the issues to be solved in next week’s article. If you’ve got any strategies for unit testing code that uses the Unity engine, please leave a comment about how you do it!