On Testability and Unity 3D

Testability is a crucial consideration when we write code, and for us it includes the ability to execute and unit test our code outside of Unity.

Unfortunately Unity throws up a few challenges here; MonoBehaviours are not testable, along with most types inside UnityEngine.dll.

This has led us to develop ‘Uniject’ – a C# testability framework for Unity that offers:

  • Plain Old C Sharp, testable MonoBehaviour equivalents
  • A robust and flexible way of creating GameObjects automatically, by inference of the code that drives them
  • Constructors!
  • An extremely flexible code base – in short, the benefits of DI + IOC.

The first attempt

Here’s how to make an untestable zombie, taken from our latest game, The Clones of Corpus.:

[RequireComponent(typeof(SphereCollider))]
[RequireComponent(typeof(AudioSource))]
[RequireComponent(typeof(NavMeshAgent))]
...
public class Zombie : MonoBehaviour {

    private AudioSource audioSource;
    ...

    void Start () {
        this.audioSource = GetComponent();
        ...
    }
}

Problems:

  • Only Unity knows how to create MonoBehaviours
  • We depend on concrete types in the UnityEngine namespace that we can’t mock out

So, how might we make our zombie testable?

The key is to break its dependence on UnityEngine*, and instead depend on interfaces which mirror their UnityEngine equivalent, supplied as parameters using the Dependency Injection pattern.

Here’s how our testable zombie looks (many dependencies omitted):

[GameObjectBoundary]
public class Zombie : Testable.TestableComponent {

    private IAudioSource audioSource;
    ...
    public Zombie(Testable.TestableGameObject obj, IAudioSource audioSource...) : base(obj) {
        this.audioSource = audioSource;
        ...
    }
}

We use Ninject, an inversion of control framework, to actually construct our objects at runtime.

A test!

We’re now working with Plain Old C Sharp Objects, here’s one of our NUnit tests:

[Test]
public void testZombieKilled() {
    Zombie zombie = kernel.Get<Zombie>();
    zombie.kineticDamage(5);
    step(1);
    Assert.AreEqual(ZombieState.DYING, zombie.getState());
}

(Not shown is the testing base class that sets up Ninject for us and provides the means to ‘step'; simulating one or more frames).

How it works

Everything we need to instantiate a zombie is declared as a constructor parameter:

TestableGameObject

This is a dependency of the TestableComponent base class. It is equivalent to the UnityEngine.GameObject class; in the same way that MonoBehaviours belong to GameObjectsTestableComponents belong to a TestableGameObject.

IAudioSource

This is identical to the UnityEngine AudioSource class. There are a number of other parameters which are not shown, such as INavMeshAgent, ISphereCollider…

Auto wiring

We configure Ninject with different Modules for running under NUnit and Unity. The NUnit module tells Ninject to use mock implementations of our interfaces, and the Unity module tells  it to use our ‘real’ implementations that wrap their UnityEngine equivalents.

The Unity Ninject module contains some special scoping to ensure our TestableComponents are translated into an appropriate GameObject hierarchy.

An instantiation

So how does our zombie actually get created when we call the following?

kernel.Get<Zombie>();

Ninject sees that our Zombie requires an instance of TestableGameObject. This is bound to a class that wraps a UnityEngine.GameObject, so Ninject creates one, and our Unity GameObject is created.

Next, Ninject tries to create our IAudioSource parameter. This is bound to a concrete class that wraps the UnityEngine.AudioSource class (a monobehaviour). This wrapper itself depends on having a GameObject to add the AudioSource to, which it takes as a constructor parameter. A custom Ninject scoping ensures that the same GameObject is supplied as was created for the TestableGameObject.

This process continues for the remaining dependencies.

Portability

An interesting consequence of this decoupling is the ease of porting our code to another game engine. To get our code running on Windows Phone 7, one would merely need to provide XNA based implementations of the interfaces in the Testable namespace.

The original Last Stand, despite being published as a pure java android game, is playable as a standalone desktop java application (and was mostly playtested this way).

Compatibility

The framework has been verified on Desktop, Android and iOS builds. Name mangling makes it unsuitable for Flash builds.

The Price!

Performance:

  • All calls to UnityEngine now go through an interface.
  • Object construction speed

Practically, we did not notice these making The Clones of Corpus.

What we did notice was the massive increase in productivity these patterns can bring, which is extensively documented elsewhere.

*Mostly, we still use some essential structs like Vector3.

About these ads

5 Responses to On Testability and Unity 3D

  1. I’d be interested in seeing the state of this and perhaps helping out on github. I’ll drop you a line in your contact form.

  2. robquint says:

    Pretty cool stuff guys and I can’t tell you have relieved I was to find a good starting point for our own testing efforts with Unity. But as I’ve been looking at the example it seems the abstraction is quite leaky. For instance if you look at IResourceLoader it is returning concrete Unity types Material and AudioClip. There seems to be other areas where this happens too.

    Looking at the information in your original post you mentioned that you ported this codebase to XNA. Did you have to plug all of those holes or did you just leave the same “essential” structs or classes in place?

    Right now I’m thinking I’ll wrap Material or other things to fill this in all of the way, but I was just curious what your thoughts were.

    • outlinegames says:

      The goal of Uniject was to support plain old C# unit testing, not to eliminate any dependency on UnityEngine.

      We use certain concrete Unity types where those types can be instantiated and used by unit tests, eg the mathematical primitives like Vector3. Where this is impossible we have extracted an interface to act as a split point for our test and run time implementations.

      The comments on XNA were an observation that Uniject is a step towards decoupling your codebase from UnityEngine. A full XNA port would indeed require you to replace Vector3, AudioClip etc, as you state. This is not something we’ve attempted.

  3. taotaojonas says:

    Great stuff!

    I tried out the project using unity and started spawning GameObjects. However, I cannot really seem to be able to create a NavMeshAgent on the object I create from a factory. Could you kindly provide an example using INavMeshAgent as well? I get it to work with IRigidBody and ISphereCollider.

    Thanks a lot!

    • outlinegames says:

      The INavMeshAgent requires working around a Unity limitation (for which I raised a bug); if a NavMeshAgent component is added to a gameobject, and that gameobject is not within range of the navmesh, the navmeshagent will enter a broken state which there is no programmatic way of detecting.

      Thus, INavMeshAgent has an ‘OnPlacedOnNavmesh’ method that should be called when the object has been correctly placed on the navmesh. This is responsible for creating the underlying Unity NavmeshAgent component; the component will not function until this is done.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: