Testing in Unity3d – Part 1

I don’t like writing tests. Do you? Well, ok, what I actually mean is that I don’t like writing unit tests. In one of my projects the assumption (and TeamCity setting) was that we’ll have 100% test coverage. There was no .net core yet, so we couldn’t write functional tests actually checking the integrations between different parts of the system and therefore we had to mock them. Oh boy, and we mocked them hard! Because of the requirement to have 100% of the code covered by tests, we had many tests doing no more than checking those mocks, because some methods were only reaching to multiple microservices and the results aggregation was done one layer above that. That’s what you get in most big enough codebases and our client wasn’t really keen on refactoring. So yep, I hated that period of time in my career (it’s actually just a tip of an iceberg, but that would be a topic for another, long post ๐Ÿ˜‰ ).

Even without the 100% test coverage requirement I’m complaining about, I still wouldn’t like writing unit tests, because in the enterprise world it’s a rare situation that you have some big chunk of business logic enclosed in one class or a method. It’s usually spread out between different layers, so you end up writing a unit test that sets some value and then checks if the value was set. Maybe in the end it will show us where the NullReferenceExceptions could be thrown etc. but that’s it. Even with 200% test coverage you still wouldn’t know if the system works as a whole. However now we have .net core and its awesome functional testing features, so in my current enterprise project its actually pretty awesome, because we can check the whole flow in a really readable manner and actually be sure that yes, the functionality works front-to-back.

Where would I complain if not in my own blog, right? ๐Ÿ™‚

When I started doing Unity3d coding I wasn’t really happy with the idea of testing my code in that environment. I saw the official tutorial showing two kinds of Unity3d tests – editor and player. The difference is that an editor test is actually a unit test and the player test is what’s normally called an integration test. In this post (and the next one) I will be focusing on the later one, because it’s harder to write integration tests than unit tests. Both in the boring enterprise world of ETLs, microservices and micromanagement and in Unity3d.

Have you read my last post? The one about creating homing missiles in Unity3d: https://mostly-unity.com/2019/01/15/how-to-make-a-homing-missile-in-unity3d/ . I think it’s a good candidate to make some tests as a warm up and then get to something bigger (in the next post).

The good news is that you don’t really need to install anything to be able to integration-test your code. Unity3d has it all built-in, along with the NUnit framework. The bad news is that for an integration test you need a scene, so the setup involves a bit more work than unit test. You can always use the actual scene the player will see, but it’s been my experience it’s better to set up another one, just to be able to move stuff around without messing with the level design. Also the less cluttered the scene is, the easier it is to spot an error.

My test scene setup looks like this:

The red cube is the weapon we are testing, plus there are two barriers. In the scene view they overlap one another, so I clicked on one of them, so that it’s easier to see.

Now, take a look at the test class:

public class HomingMissileWeaponBehaviourTests {

    private readonly string _sceneName = $"{nameof(HomingMissileWeaponBehaviourTests)}Scene";
    
    protected GenericWeaponBehaviour _weapon;
    protected GameObject _target;
    protected GameObject _barrier;
    protected GameObject _nextBarrier;

    [UnityTest]
    [Description(@"
        Given a loaded weapon
        And a target
        When there's a barrier in between the weapon and the target
        And the barrier is close to the weapon
        Then the weapon is not targetting object
        And the weapon is not able to target object
    ")]
    public IEnumerator WeaponIsNotAbleToTargetObjectWhenAnObstacleIsVeryClose() {
        yield return LoadLevel(_sceneName);

        _barrier.transform.position = new Vector3(_barrier.transform.position.x, _barrier.transform.position.y, 0);

        var isTargettingObject = _weapon.IsTargettingObject(_target.transform);
        var canSeeTarget = _weapon.CanTargetObject(_target.transform, performAngleCheck: true);

        Assert.IsFalse(isTargettingObject);
        Assert.IsFalse(canSeeTarget);
    }
    
    protected IEnumerator LoadLevel(string sceneName) {
        var loadSceneOperation = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single);

        loadSceneOperation.allowSceneActivation = true;

        while (!loadSceneOperation.isDone) {
            yield return null;
        }

        _weapon = GameObject.Find("weapon").GetComponent<GenericWeaponBehaviour>();
        _target = GameObject.Find("target");
        _barrier = GameObject.Find("barrier");
        _nextBarrier = GameObject.Find("barrier2");

        _nextBarrier?.SetActive(false);
    }
    
}

The test first loads the level, which includes setting the “actors” – the target, the two barriers and the weapon. It then disables the second barrier, because it’s not required in the test method you see above. I’ve given the test a description for the sake of readability. It’s a habit I took from writing functional tests using SpecFlow – I really like those long descriptions stating what exactly we’re doing in a given test and what we expect to achieve.

The next thing the test does is it moves the _barrier to the world center. Our weapon is very close to (0, 0, 0) vector and since we’re checking if the weapon is not able to target the _target, we need to move the _barrier like this.

Now, a comment about the logic being tested here. As you can see, I’m actually using two methods – IsTargettingObject and CanTargetObject. All the weapons in my game extend a base class named GenericWeaponBehaviour that defines both of them as virtual. The first method checks if the weapon is rotated in the direction of the target and if there’s an unobstructed, straight path between the two. The second one checks if the weapon can be rotated in a way that will enable it to actually target the given object. In the case of a weapon capable of shooting homing missiles the second function relies on the first one, because if there’s something directly in front of the weapon (given that the rocket is longer and wider than, say, machine gun bullet), shooting a missile will make it explode almost instantly because of hitting the barrier.

Aaaaand this is actually a unit test ๐Ÿ™‚ . However to make raycasts (and that’s what the two weapon methods mentioned above are doing) you need to be actually running your code and that’s why the UnityTest usage.

Now, an actual integration test:

[UnityTest]
[Description(@"
    Given a loaded weapon
    And a target
    When there's a barrier in between the weapon and the target
    And the weapon shoots a homing missile in the target's direction
    Then the projectile avoids the barrier
    And reaches the target
")]
public IEnumerator MissileAvoidsObstacleAndHitsTarget() {
    yield return LoadLevel(_sceneName);

    var targetHit = false;

    _barrier.transform.position = new Vector3(_barrier.transform.position.x, _barrier.transform.position.y, 8);
    _barrier.transform.localScale = new Vector3(_barrier.transform.localScale.x, 100, _barrier.transform.localScale.z);
    _target.GetComponent<CollisionReceptor>().OnCollision = c => {
        targetHit = true;
    };

    var projectiles = _weapon.ShootAt(_target.transform);

    yield return new WaitUntil(() => !projectiles[0].activeSelf);

    Assert.True(targetHit);
}

The code above checks if a missile has reached its target. First it moves the _barrier so that the weapon will be able to shoot. It also makes it bigger, so that we can be sure that the homing missile mechanism is working properly and that it is able to pass obstacles by. Then it sets the collision detection so that it can later check if the target has been hit. Two lines more and we reach the yield return statement. You will be doing this a lot in your tests, and the more complex the logic you’re checking, the more of those will be there. Essentialy we need to wait for the missile to get destroyed, to make an assertion and that’s what is happening here.

To sum this all up: I believe that writing such tests in Unity3d is not a matter of choice but rather a thing you really need to do. It will save you a world of pain, because the other way of testing such interactions and logic is just playing your game. In a complex enough environment you would loose whole days trying to reproduce bugs that could be easily spotted had you created a test. So yep, you guessed it – I really love testing in Unity3d ๐Ÿ™‚

I’m aware this was pretty basic, but as I said – it’s just a warm up. In the upcoming post, I’ll show you how I’m going about testing my game AI.

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 )

Google photo

You are commenting using your Google 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 )

Connecting to %s