TLDR version

  • Always test external (public) behaviours
  • Humble Object design pattern: take the class that you want to test and isolate as much code outside in a new separate class, leaving only a humble object (one that is too simple to need any testing) and a new object that now no longer has any complex dependencies (Monobehaviour) that is fully testable
  • This can be used even with the above or without it - Extract an interface from the object or behaviour you want to test
  • Using nsubstitute, implement the interface in your tests, creating a mock for what you are testing, thus linking the test and the class that is being tested through the interface

The full story

Introduction

This is a long story about how I came to understand how to implement a unit test for a simple unity project (pong).

The concepts that I had to go through so I could finally create unit tests are: encapsulation, end to end testing, functional testing, test driven development, behaviour driven development, unit tests, manual testing, c# classes, c# interfaces, nsubstitutes, mocking and the humble object design pattern.

That is because, after having started my third project, a pong clone, I also wanted to dig a bit into automatic software testing and TDD and see if I can use something from it all. But first, a small note on encapsulation, since this was something I was already doing and it had an impact in my test-driven adventures.

Encapsulation

Of all the places I could have started, I decided to start here. See, when I was a young pup who had just finished university, I thought I would become a programmer. I had done a few courses and I had this “Learn C in 21 days” book (that was bigger than an encyclopedia). In my naive mind, the possibilities were endless. Thus I started learning, and the one thing that I remember from those days more than 15 years ago is “always use private for everything, only use public when you really need it”.

Software testing

I had some ideas of what it meant, but for thoroughness’ sake, I wanted to dig deeper and get a better understanding so I can make an informed decision on what I could deem useful for the road ahead.

On the WHY of testing

If there is one thing that I really dislike from trying to build a game, oh well it’s manual testing. The mind numbing repetition, the wasted time of it all, the attention that’s required. And then you change one line of code and you just need to start all over again. And the problem only gets worse with the increase in complexity. So when something promises to take a part of those pains away, making your code better, more secure and easier to refactor, well I thought it’s definitely worth it.

On the WHEN of testing - TDD

TDD or test driven development - a technique through which the developer first writes a failing test for non-existing code, then writes the code, then re-runs the test and checks that all is fine and then goes to the next test for another non-existing block of code and so on. Since my brain can’t even fully wrap around most basic C# concepts, I immediately decided that asking it to actually anticipate said concepts and start with writing code for code I have very little idea about how I will actually implement is just pure overkill.

On the WHAT of testing - Unit tests

There are many types of tests that have different scopes. The unit test is the smallest, most basic one and seemed like a good place to start for a newbie such as myself. My initial understanding of a unit test was a test designed to verify the correct operation of the smallest possible unit of code (usually a method). Each test should verify a single explicit assumption about the target functionality. In my current understanding, I would a most important thing that had escaped to me, and that is that the behaviour to be tested should be external only (public). (I spent quite a bit of time searching the forums and stackoverflow on how I can invoke private methods in my tests…).

On the HOW of testing - BDD

BDD is behaviour driven development and from what I could gather it comes from TDD. The main idea is to use your tests to focus on behaviour, not technical details. Ex: “1+1 = 2” vs “is there an addition taking place?”. For BDD to be implemented correctly, naming conventions are really important.

Pong’s bouncing ball

Implementation

At this point I decided to start working on my game and then see how I can create some unit tests for it. Here’s an outline of how it would look like when coding:

Roll the Dice

The ball class seemed like a good place to start, so here is my first implementation attempt:

public class Ball : MonoBehaviour
{
    Rigidbody2D rigidBodyComponent;
    bool isColliding = false;
    float speed = 10f;
    Vector2 direction;


    void Start()
    {
        rigidBodyComponent = GetComponent<Rigidbody2D>();
        
        direction = InitializeBallDirection();
        rigidBodyComponent.velocity = direction * speed;
    }


    void FixedUpdate()
    {
        if (isColliding)
        {
            speed += 1f;
            rigidBodyComponent.AddForce(rigidBodyComponent.velocity.normalized * speed);
            isColliding = false;
        }
    }


    // Sent when an incoming collider makes contact with this object's collider
    void OnCollisionEnter2D()
    {
        isColliding = true;
    }


    Vector2 InitializeBallDirection()
    {
        float yCoord = 0.4f;
        Vector2 newDirection;

        if (Random.value <= 0.25f)
        {
            // Random left lower direction
            newDirection = new Vector2(-1, Random.Range(-1f, -yCoord));
        }
        else if (Random.value > 0.25f && Random.value <= 0.5f)
        {
            // Random left upper direction
            newDirection = new Vector2(-1, Random.Range(yCoord, 1f));
        }
        else if (Random.value > 0.5f && Random.value < 0.75f)
        {
            // Random right lower direction
            newDirection = new Vector2(1, Random.Range(-1f, -yCoord));
        }
        else
        {
            // Random right lower direction
            newDirection = new Vector2(1, Random.Range(yCoord, 1f));
        }

        return newDirection;
        
    }
}

The first unit test - and the first problem

The ball worked as expected and I was happy. So I decided to write my first unit test. I figured a good behaviour I would verify is if the ball starts up with a random direction, so the InitializeDirection() method became my target.

And my test became my curse: First attempt:

            [Test]
            public void InitializeBallDirectionAndSpeed_BallIsSittingInOnePlace_ErrorThrown()
            {
                Ball testBall = new Ball();
                Vector3 initialBallCoords = testBall.transform.position;

                testBall.Invoke("InitializeBallDirectionAndSpeed", 1f);
                Vector3 presentBallCoords = testBall.transform.position;

                Assert.AreNotEqual(initialBallCoords, presentBallCoords);
            }

Gives the error: You are trying to create a MonoBehaviour using the 'new' keyword. This is not allowed. MonoBehaviours can only be added using AddComponent()...

Second attempt just doesn’t work (without any errors), so I kept digging.

[Test]
        public void InitializeBallDirectionAndSpeed_BallIsSittingInOnePlace_ErrorThrown()
        {
            Ball testBall = new GameObject().AddComponent<Ball>();
            Vector3 initialBallCoords = testBall.transform.position;

            testBall.Invoke("InitializeBallDirectionAndSpeed", 1f);
            Vector3 presentBallCoords = testBall.transform.position;

            Assert.AreNotEqual(initialBallCoords, presentBallCoords);
        }

The actual problem is that I can’t really instantiate a test ball of type Ball for my tests. In order to be able to create and successfully run a test, I need to first create a mock object or somehow get rid of the Monobehaviour dependency. To that end, I discovered something really useful - the Humble Object design pattern.

Solution - The Humble Object design pattern

This design pattern says that if you have an object that has complex dependencies (like Monobehaviour), you should isolate as much code as you can from it into a new object, thus leaving the original object too humble and simple for you to need to test and the new object fully testable.

Getting back to my ball, I decided to apply this pattern and refactored the code, leaving me with two classes:

A simple, humble version of Ball

public class Ball : MonoBehaviour
{
    Rigidbody2D rigidBodyComponent;
    bool isColliding = false;
    float speed = 10f;
    public Vector2 Direction { get; set; }
    BallInitializeDirection newDirection;


    void Awake()
    {
        newDirection = new BallInitializeDirection();
    }


    void Start()
    {
        rigidBodyComponent = GetComponent<Rigidbody2D>();
        Direction = newDirection.InitializeDirection();

        //direction = InitializeBallDirection();
        rigidBodyComponent.velocity = Direction * speed;
    }


    void FixedUpdate()
    {
        if (isColliding)
        {
            speed += 1f;
            rigidBodyComponent.AddForce(rigidBodyComponent.velocity.normalized * speed);
            isColliding = false;
        }
    }


    // Sent when an incoming collider makes contact with this object's collider
    void OnCollisionEnter2D()
    {
        isColliding = true;
    }
}

A Monobehaviour-free, fully testable version of my target method

public class BallInitializeDirection
{
    public Vector2 InitializeDirection()
    {
        float yCoord = 0.4f;
        float randomValue = Random.value;
        Vector2 newDirection;
    
        if (randomValue <= 0.25f)
        {
            // Random left lower direction
            newDirection = new Vector2(-1, Random.Range(-1f, -yCoord));
        }
        else if (randomValue > 0.25f && randomValue <= 0.5f)
        {
            // Random left upper direction
            newDirection = new Vector2(-1, Random.Range(yCoord, 1f));
        }
        else if (randomValue > 0.5f && randomValue < 0.75f)
        {
            // Random right lower direction
            newDirection = new Vector2(1, Random.Range(-1f, -yCoord));
        }
        else
        {
            // Random right lower direction
            newDirection = new Vector2(1, Random.Range(yCoord, 1f));
        }
    
        return newDirection;
    }
        
}

And now I can write working tests, because I am instantiating a class that has no complex Monobehaviour dependencies:

[Test]
            public void DirectionShouldBeRandomAtStart()
            {
                Vector2 initialDirection;
                Vector2 newDirection;
                BallInitializeDirection testDirection = new BallInitializeDirection();

                initialDirection = testDirection.InitializeDirection();
                newDirection = testDirection.InitializeDirection();

                Assert.AreNotEqual(initialDirection, newDirection);
            }


            [Test]
            public void DirectionShouldNotBeHorizontalAtStart()
            {
                Vector2 direction;
                BallInitializeDirection testDirection = new BallInitializeDirection();

                direction = testDirection.InitializeDirection();

                Assert.AreNotEqual(0, direction);
            }

It was at this point that I realized that in order to test something, that something needs to be public. So I should change my thinking from “this looks like something I could test” to “this is external (public) behaviour, I should write tests for it”. I just need to combat my insecurity with my lack of knowledge and ignore the need to test everything for a while…

A graphic version of the entire thing:

Roll the Dice

Solution 2 - Implementing an interface and using mocking (incomplete)

Even though I had a working solution and a new great tool in my understanding of how to code for my games, there was another question - what do I do when, for any number of reasons, I can’t apply the Humble Object pattern to the Ball class (or the class with complex dependencies)? The answer is by using a technique called mocking. There are two ways of doing it - the manual way (which I will just ignore since it introduces a new object that must be maintained) and the automatic way through NSubstitute.

A mock, by definition, is something that was made as an imitation. Mocking is the process of creating objects that simulate the behaviour of real objects. It allows for the testing on one unit of code without being reliant on its dependencies. Mock objects have the same interface as the real objects they mimic, allowing the client object to remain unaware if it is using a real object or a mock object. (I feel obliged to stress that I am talking about general concepts in this paragraph and not exact definitions like test double, fake object, mock object, stub object, etc - for those see the links at the end but be warned, there doesn’t seem to be a general consensus about naming).

Mocking is achieved through the use of interfaces. The interfaces connect all the elements involved. But what is an interface? An interface is like a class, with the difference that it only states what needs to be instantiated, not how.

Unfortunately this is as far as I’ve got on this second solution, and after spending many hours reading a lot of material I feel it would be inefficient to continue on without trying to actually implement more of the theory I went through.

A helpful guy told me on discord ( @mariandev96 ): “Make a habit out of writing, modifying and running the tests. Keep them up to date with the code changes you make so that you can rely on them.” and that was good advice. That, along with the promise that I will return one day in the future to finish this, is a good note to end this article on.

General knowledge about software testing and practices

Testing in the context of games and unity

C# concepts