Last week’s article showed a technique that you can use to abstract the Unity engine so that you can test code that uses it. Today’s article presents another technique that allows you to remove this abstraction layer so your game code is faster and more natural. Read on to learn how!

To briefly recap, the technique discussed in last week’s article involves introducing an abstraction layer between your game code and the Unity engine. Instead of directly using WWW, your game code would use an IWww interface implemented by a UnityWww class full of one-liner passthroughs to a real WWW it holds as a private field.

This approach works, but it’s a lot of boilerplate code, complicates the Unity API by introducing a parallel version of it, and adds a virtual function call every time you use the Unity API. It can lead to some extremely strange situations, like wrapping Vector3 because it contains calls into the Unity engine’s native code. So today’s article is all about an alternative technique that helps out with some of these problems.

When we put C# files into our project’s Assets directory, they’re compiled by Unity’s Mono compiler into a DLL: Library/ScriptAssemblies/Assembly-CSharp.dll. That DLL has a reference to the UnityEngine.dll that ships with Unity to provide the API for our scripts, including classes like WWW. Normally the Unity engine loads up our Assembly-CSharp.dll and does the linking to Unity’s UnityEngine.dll so it’s ready for us to use the Unity engine’s API. When we’re unit testing, we don’t want to use the real UnityEngine.dll. That’s why we introduced the abstraction layer last week.

Instead of an abstraction layer, we can instead swap out UnityEngine.dll itself. When unit testing, we could use an alternative version of UnityEngine.dll that’s implemented in such a way that WWW doesn’t really make web calls, and so forth for other classes like Input and MonoBehaviour. That’s basically what we did last week with NSubstitute when we wrote code like this:

var www = Substitute.For<IWww>();
var done = false;
www.isDone.Returns(x => done);
www.bytes.Returns(new byte[]{ 1, 2, 3, 4, 5 });

We were providing an alternative implementation of WWW that did something specific for our tests. Importantly, it didn’t make any web calls.

It turns out that making our own UnityEngine.dll is quite easy! All you have to do is make a new DLL project in MonoDevelop or Visual Studio. I wrote a guide last year with the basic steps. Just name the DLL UnityEngine.dll and start filling it with types that match the Unity API. For example, you could start with an empty version of WWW:

public class WWW : IDisposable
{
	public WWW(string url) { }
	public bool isDone { get; set; }
	public byte[] bytes { get; set; }
	public void Dispose() { }
}

Now that you have a fake UnityEngine.dll, your game code (Assembly-CSharp.dll) and unit tests (Assembly-CSharp-Editor.dll) can use it instead of Unity’s real version. To do this, it’s again helpful to package your game code and unit test code as DLLs. In Visual Studio or MonoDevelop, your solution should look like this:

MyGame // solution
|-UnityEngine // fake UnityEngine.dll
  |-WWW.cs // implement the Unity API
|-Runtime
  |-References
    |-UnityEngine // reference the UnityEngine project
  |-WebCallRunner.cs // your game code
|-UnitTests
  |-References
    |-UnityEngine // reference the UnityEngine project
  |-TestWebCallRunner.cs // your unit tests

To run the unit tests from MonoDevelop, use the Run > Run Unit Tests menu option. Visual Studio users will need to install either the “Visual Studio Test Adapter” or “dotCover” from the “Resharper” suite and follow the relevant instructions.

To use the game code for real in the Unity engine, make sure the DLL your IDE built is in your Unity project’s Assets directory. The easiest way to achieve that is to build it directly there and make Unity ignore the fake UnityEngine.dll. Here are some steps for MonoDevelop:

  1. Open MonoDevelop
  2. Right-click the runtime game code project and click Options
  3. Click Build > Output on the left side
  4. Change the output path to your Unity project’s Assets directory
  5. OK, build project
  6. Open the Unity editor
  7. Click “UnityEngine” in the Project pane
  8. Uncheck “Any Platform” and then all the platforms, click Apply

Now that you’ve got the project set up, let’s look at how to make the fake UnityEngine.dll more useful. We started with an empty WWW implementation and that’s enough to get our WebCallRunner class from the last article to compile. It’s not a very useful class though since there isn’t a very easy way to write our test. Let’s revisit how the test looked when we were using NSubstitute to fake an implementation of IWww:

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();
	}
}

Let’s break down what features we’d like our unit tests to have access to so that we can write good tests:

  • Have a lambda called when a Unity API function is called
  • Set the return value of Unity API functions
  • Check how many times Unity API functions were called
  • Check the parameters passed to Unity API functions, including constructors

NSubstitute handles all of that except recording the calls to constructors. Thankfully, it’s pretty easy to write a class that emulates all this functionality and include it in our fake UnityEngine.dll using an alternate namespace:

using System;
using System.Collections.Generic;
 
namespace TestUnityEngine
{
	/// <summary>
	/// Records calls to a Unity API and calls a delegate each time
	/// </summary>
	/// <typeparam name="TParam">Type of parameter passed to the Unity API<typeparam>
	/// <typeparam name="TReturn">Type of return value the Unity API returns<typeparam>
	/// <author>Jackson Dunstan, http://JacksonDunstan.com/articles/3706</author>
	/// <license>MIT</license>
	public class ApiCalls<TParam, TReturn>
	{
		/// <summary>
		/// History of calls to this API
		/// </summary>
		public List<TParam> History = new List<TParam>();
 
		/// <summary>
		/// If not null, called by <see cref="Call"/> with its parameter and used as the return val
		/// </summary>
		public Func<TParam, TReturn> OnCall;
 
		/// <summary>
		/// Record a call in <see cref="History"/> and return the default value for
		/// <see cref="TReturn"/> if <see cref="OnCall"/> is null or call <see cref="OnCall"/>
		/// with <see cref="param"/> and return its return value.
		/// </summary>
		/// <param name="param">Parameter to record in <see cref="History"/> and pass to
		/// <see cref="OnCall"/> if it's not null.</param>
		public TReturn Call(TParam param)
		{
			History.Add(param);
			return OnCall == null ? default(TReturn) : OnCall(param);
		}
	}
}

For properties and functions that take no parameters or return void, it can be helpful to have a dummy type:

namespace TestUnityEngine
{
	/// <summary>
	/// An empty type to represent 'void'
	/// </summary>
	/// <author>Jackson Dunstan, http://JacksonDunstan.com/articles/3706</author>
	/// <license>MIT</license>
	public struct VoidType
	{
		/// <summary>
		/// An instance of this type. It is essentially a global constant like Math.PI.
		/// </summary>
		public static VoidType Val;
	}
}

Now we can rewrite the WWW class to use ApiCalls like so:

using System;
 
using TestUnityEngine;
 
namespace UnityEngine
{
	public sealed class WWW : IDisposable
	{
		public ApiCalls<string, VoidType> UrlConstructorCalls = new ApiCalls<string, VoidType>();
		public ApiCalls<VoidType, byte[]> BytesGetterCalls = new ApiCalls<VoidType, byte[]>();
		public ApiCalls<VoidType, bool> IsDoneGetterCalls = new ApiCalls<VoidType, bool>();
		public ApiCalls<VoidType, VoidType> DisposeCalls = new ApiCalls<VoidType, VoidType>();
 
		public WWW(string url)
		{
			UrlConstructorCalls.Call(url);
		}
 
		public byte[] bytes
		{
			get { return BytesGetterCalls.Call(VoidType.Val); }
			private set { }
		}
 
		public bool isDone
		{
			get { return IsDoneGetterCalls.Call(VoidType.Val); }
			private set { }
		}
 
		public void Dispose()
		{
			DisposeCalls.Call(VoidType.Val);
		}
	}
}

The new WWW implementation just forwards along every call to a public ApiCalls instance. This allows ApiCalls to act like a substitute created by NSubstitute. The unit test now looks like this:

using System;
 
using NUnit.Framework;
 
using UnityEngine;
 
using Runtime;
 
namespace TestUnityEngineExamples
{
	[TestFixture]
	public class TestWebCallRunner
	{
		[Test]
		public void RunYieldsUntilWwwIsDoneThenDispatchesEventAndDisposesWww()
		{
			var www = new WWW("url");
			var done = false;
			www.IsDoneGetterCalls.OnCall += v => done;
			www.BytesGetterCalls.OnCall += v => 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);
			Assert.That(www.DisposeCalls.History, Is.Empty);
 
			// still not done, so keeps going
			Assert.That(enumerator.MoveNext(), Is.True);
			Assert.That(dispatchedBytes, Is.Null);
			Assert.That(www.DisposeCalls.History, Is.Empty);
 
			done = true;
 
			// done, so enumerator stops, callback is called, WWW is disposed
			Assert.That(enumerator.MoveNext(), Is.False);
			Assert.That(dispatchedBytes, Is.EqualTo(new byte[]{ 1, 2, 3, 4, 5 }));
			Assert.That(www.DisposeCalls.History.Count, Is.EqualTo(1));
		}
	}
}

Notice how similar the unit test looks to the version that was using NSubstitute! There are only a few differences to point out:

// Get an instance of a mock/fake/substitute class
var www = Substitute.For<IWww>(); // NSubstitute
var www = new WWW("url"); // ApiCalls
 
// Have a lambda called when a Unity API function is called
www.isDone.Returns(x => { /* do something */ }); // NSubstitute
www.IsDoneGetterCalls.OnCall += v => { /* do something */ }; // ApiCalls
 
// Set the return value of Unity API functions
www.isDone.Returns(x => done); // NSubstitute
www.IsDoneGetterCalls.OnCall += v => done; // ApiCalls
 
// Check how many times Unity API functions were called
www.Received(1).Dispose(); // NSubstitute
Assert.That(www.DisposeCalls.History.Count, Is.EqualTo(1)); // ApiCalls
 
// Check the parameters passed to Unity API functions, including constructors
Received.InOrder(() => { www.Foo("a"); www.Foo("b"); www.Foo("c"); }; // NSubstitute (constructors not possible)
Assert.That(www.FooCalls.History, Is.EqualTo("a", "b", "c")); // ApiCalls

All in all, writing unit tests using ApiCalls is about the same as using NSubstitute. It’s also really easy to set up the Unity classes to use ApiCalls since a one-liner is all that’s required.

That’s all there is to this technique. To compare it with the previous technique, here’s a handy table:

Abstraction Layer Fake UnityEngine.dll
Requires middleman API Yes No
Requires factories Yes No
Boilerplate code 4 files per type: interface, class/struct, factory interface, factory class/struct 1 file per type: class/struct in UnityEngine.dll
Code coverage All but abstraction layer All but UnityEngine.dll
CPU overhead 1 virtual function call per API call None
Project complexity Abstraction layer noise Solution with three DLL projects

This technique wins pretty handily against the abstraction technique in most categories, but loses in terms of project complexity. Nothing can really compete with the simplicity of just putting .cs files in your Assets directory, and that’s a downside with this approach. However, if you want to gain the upsides from the comparison table it may be the approach for you.

What do you think of these two techniques? Do you have a third way to do it? Let me know in the comments!