It’s been so long that I feel a newcomer to WordPress’ user interface.

A few years ago, just after the last Italian Agile Day I attended (2013), I was thinking of writing something about using TDD when developing in Unity.

Recently I saw an email passing by the tdd mailing list asking about exactly this topic and, as it happens, I’m on holiday right now, so I finally got around at writing the article I should have written years ago. What a serendipitous accident.

First some notes :

  1. This is not about testing pure “unity-neutral” C# code. In Unity, at some point, you start using objects which have no relationship on Unity types (like MonoBehaviour, Transform etc..), but this happens quite low in the call stack and, at times, not at all. Besides, you can test drive those with normal tooling, describing that is redundant to any good tdd book.
  2. It is unlikely that many of your interesting game features will be described entirely within the context of unity-neutral code. Unity is pervasive and it is not built for technological isolation (probably my biggest issue with a product I otherwise love). After all, if you are writing a 3d game, most of the stuff you need to code is touching concepts like a transform, a collision, an animation; it is possible to express them neutrally, but Unity has decided to sacrifice isolation for immediacy. I’ve tried reintroducing that isolation and it’s not nice, I thus do it very selectively.
  3. A lot of the design questions you need to answer when developing in Unity are related to the distribution of responsibilities among the MonoBehaviours you attach to GameObjects (Unity’s game logic is structured around the Extension Object Pattern, see Gamma’s paper in PLoP 96). Skipping those parts of your logic just because they are unity-dependent pauperises tdd in Unity into irrelevance.
  4. Since a lot revolves around which MonoBehaviour of which GameObject does what, the collaborations between those behaviours are equally critical to your design; those collaborations are wired into life by Unity’s declarative dependency-injection mechanism: the Scene. The Scene is thus the seed of all your fixtures; trying to bypass it, while possible (factories, resource load, stubs), is often not worth it.

Now, all of the above is an admission that, if I want to apply my usual holistic approach to tdd, most of my tests will not look like unit tests and will need to cope with Unity’s environment.

It took me some time to accept this fact, but when I did, I started to see that there were interesting advantages in accepting a Unity scene as my test runtime; the rest of this post will be about how exploiting those advantages shaped my approach to doing tdd in Unity.

You can and should simulate unit test isolation by exploiting physical space locality

This means that if the Scene is your runtime, if you take care to build your test in such a way that its effects stay within a well-defined volume, your Scene will behave similarly to a suite of properly code-isolated tests in classic unit testing.

This has the side effect of forcing me to avoid as much as possible world-spanning searches of other objects through tags or names: all of my GameObject-to-GameObject interaction is defined by colliders and explicitly injected dependencies. The effort to keep the tests isolated in space is already influencing my design.

After a while I’ve come to actually materialise the bounded volume of a test with a cage. This makes boundary enforcement more natural while building and running the test (you can’t ignore that something is leaving the test volume when that volume is graphically represented) and has the nice benefit of giving your test scene the look of a well-managed zoo :

the-zoo

If I really feel hardcore I can use a more advanced kind of cage that has colliders for walls, these colliders destroy anything that they touch and throw an exception, failing the test. Frankly, while cool, this is overkill if you run your test Scene in Unity and glance at what is happening, but I believe that if the tests are meant to run in a headless runtime (not even sure if it is possible), say, within a CI build, those exception-throwing cage walls become necessary: they are the only way to spot an abusive test early on, before it pollutes other tests.

A final note on the physical space isolation : if you look at what Unity has provided as automated testing tools, they have taken a different approach. Every test is run sequentially and, while it runs, only the GameObject representing the test (and its hierarchy) exists; the test can thus play with the whole empty scene, without limitations. I was already using my “caged” approach before Unity published the testing tools, so I am biased to my solution, but I can articulate a bit why I prefer my solution : first, the limitation of having everything present in the scene at the same time while running the tests informs my design, as I explained above: it ensures that my logic is intrinsically bounded in space and not world-spanning; second, my approach runs all tests at the same time, which is critical when most of your tests require one or two seconds to pass to let objects move around; I can run dozens of tests within a few seconds, with everything happening at the same time.

Here you can see what happens on the ground floor of my test zoo within a few seconds, a dozen tests doing their thing at the same time :

 

The cage must contain only the (Game)Objects you want to test

The problem with not limiting the test to pure, unity-neutral logic, is that the test can grow to become a monster. Just like a classic unit test should not setup the whole system with dozens of components only to test a specific case, I always make sure that I can setup a minimum amount of components, all of them neatly bounded by the cage, and still test what I need to test. If my design is correct, I will be able to demonstrate the feature I want with only the objects that will contain the feature.

This quality is core. Failing this, the tests are not declaring a unit of expected behaviour and everything devolves to automated smoke tests. Interesting, but big, clunky and of little value as a design tool.

Here’s how I built the test that brought the “Planter” and the “ConstructionZone” concepts into my code. This is the very first cage I built for this game (it’s about city construction, in case you were wondering) :

planter-and-construction-zone

The test goal is to declare that the planter tool, when triggered by the user’s finger touching a construction zone, builds terrain and a building. I created a construction zone as a collider (the bottom, green square) and a dummy finger (the yellow line), replacing the user mouse or touch, that “clicks” on the square at Start. See below.

planter-click

Then I created a second collider, the top, green cube highlighted below, which contains an assertion that succeeds if a “building” collides with it.

planter-check

The solution to this test has been to implement two MonoBehaviours, one attached to the “finger”, the Planter tool, the other attached to the bottom collider, the ConstructionZone.

Once everything is working the construction zone and the planter tool spawn a building as soon as the finger “clicks”, the building collides with the assertion collider and the test passes. If something goes wrong, no building, no collision, failure exception in the Unity console.

Below is the result that appears when everything is fine. The building is the white cylinder, admittedly ugly, but that’s not the point.

planter-result

Below is the setup of the finger and the zone, showing how few and simple the objects involved are (the Dummy Finger and Tool User are the classes I developed to act on behalf of the user, the TestBuilding referenced in the Planter is the white cylinder).

After this first test was succeeding I moved on to refine the behaviour of the Planter by, for instance, stating that it is not affected by any obstacles, which is a characteristic I need on every tool available to the user : if it touches an action zone it doesn’t matter if it first intersects a cloud or other piece of landscape, it must still trigger it. Below you see the second cage : the flat, solid white panel is the obstacle which the Planter must ignore to touch the zone below and spawn the building. The rest of the test logic is the same as the first cage, except that, by this time, I had also created the pavement mesh, with its nice grass & ground texture that you can see below the building in the previous test result, and as such I could place the assertion collider below, where the pavement spawns, and I don’t need to actually spawn an ugly cylinder, the pavement collides and passes the test.

obstacle

Much later on, after I had completed most of the building logic, I moved to develop artillery, with tests that look like the one below. Here you can see the cannon ball flying towards a rotated cube, where I attached my “Structure” MonoBehaviour that will get damaged (depending on the angle) by the ball colliding with it.

The assertion is also sitting on the cube, waiting for a collision and checking that the Structure’s health is lower than the initial health.

cannon-ball-damages-structure

You must write your own assertion and dummy player logic to simulate every interaction you need

What I did start to use out of Unity’s testing tools are some of the assertion components, but they are far from sufficient and anyway I always need to write custom scripts to generate the actions and transient behaviours that I need to simulate events that happen in the game and that my logic needs to react to.

For instance, in the movie below I’m testing that, even if the user wants to fire, the cannons on the wall will actually fire only when a target is in firing range.

The piece of pavement that moves on the left is the target; it contains a small script (part of my testing utilities for this game) that moves it at specific intervals by a specific amount. I called it the KinematicMover (since it does not use physics to move the object). “Using” statements edited out.

public class KinematicMover : MonoBehaviour {

        public Vector3 movement;
        public int steps;
        public float pause = 1;
        private Delay delay;

        void Start() {
                delay = gameObject.AddComponent<SystemDelay>();
                delay.repeat(steps, pause, () => this.transform.Translate(movement));
        }
}

Some of the test harness logic you create will likely stay such, like the assertion checking that there were indeed some cannon balls flying within a collider during a specific time window. It is attached to the middle collider, to have the test pass of fail depending on the timing of the cannons firing from the wall.

public class ProjectileChecker : MonoBehaviour {
         
         public float windowStart = 0f;
         public float windowEnd = 10f;
         private bool complete = false;
         
         void OnTriggerEnter(Collider other) {
                 if(complete) return;
                 if(other.gameObject.GetComponent<Projectile>()) {
                         this.complete = true;
                         if(Time.fixedTime > windowStart && Time.fixedTime < windowEnd) {
                                 IntegrationTest.Pass(this.gameObject);
                         } else {
                                 IntegrationTest.Fail(this.gameObject);
                         }
                 }
         }

        void Update() {
                if(complete) return;
                if(Time.fixedTime > windowEnd + 1) {
                        this.complete = true;
                        IntegrationTest.Fail(this.gameObject);
                }
        }
}

On the other hand I’ve found that, even more frequently than in classic non-unity tdd, some of the logic driving the events for your tests turns out to be very useful game logic of its own; for instance, the gunner logic that tries to fire all the time became almost instantly part of the game’s basic opponent AI. Meet the Aggressive Gunner :

public class AggressiveGunner : MonoBehaviour {

        public City city;
        
        void Update() {
                city.fire();
        }
}

In short, you must not be scared to create quite a bit of test stubs, custom assertions, movers and shakers. They are key to well isolated and focused tests, while at the same time potentially reusable in the main code itself. They should be easy to write, if your design is ok.

Conclusion

My tdd approach in Unity ends up being what many people would define as very granular, very isolated integration testing that only later on, for very specific logic, gets down to pure C# tests. It does work pretty well and produces almost all of the design feedback I need, along with a nice test Scene(s) that makes me feel safe and in control as the game logic gets more complex.

Advertisements

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