Unit testing part 2 – Unit testing MonoBehaviours
As promised in my previous blog post Unit testing part 1 – Unit tests by the book, this one is dedicated to designing MonoBehaviours with testability in mind. MonoBehaviour is kind of a special class that is handled by Unity in a special way. Every time you try to instantiate a MonoBehaviour derivative you will get a warning saying that it’s not allowed. Being a good boy-scout and not ignoring the warning (ignoring a warning is bad in the long term!) you might have asked yourself the question, how can I mock MonoBehaviour then? The good news is that you don’t have to! Let me introduce you to…
The Humble Object Pattern
If you’ve already tried to write tests, you’ve probably stumbled upon some of the natural enemies of unit testing like UI, legacy code, bad design with no source-code access or areas with a high degree of concurrency. What make these parts hard to test? Achieving isolation: separating what is being tested from the context. There are tools out there that can help for legacy code, but for new code a very simple pattern can be used: The Humble Object Pattern.
The idea behind this pattern is very simple. Whenever you want to test a component that has any dependencies that are hard to test, extract all the logic from the component to a separate, decoupled (thus testable) class and then reference it. In other words, the problematic component (with a dependency that makes test authors’ lives miserable) becomes a very thin layer of code that has as little logic code as possible with all logic operations delegated to the newly created class.
From a state where the test has an indirect dependency to the untestable component…
…we got to a state where the test is not even aware of the bad (well, just untestable) code:
That’s pretty much it. It’s a no-brainer to be honest.
Games vs testability
What makes games so special in term of code and testability? How is testing games different from testing other software? Personally, I consider games as a pretty sophisticated pieces of software. It would be naive to say games aren’t that much different from the software you use every day. In games (with exceptions of course) you will find shiny and polished graphics, background music and other well-engineered sound samples. Games often need to handle realtime input, potentially from a variety of sources, as well as a range of output devices (read resolution). Non-functional requirements can be also more strict for games. Multiplayer games will require you to have a reliable, synchronized network connection while, at the same time, keeping the performance you need to maintain a constant frame rate.
This can make for a complex system that touches on many different kinds of media and technologies. For me, games were always masterpieces of software end-product, with some of them aspiring to be recognized as pieces of art (in the classical, visual way as well as the technical, behind-the-scenes side).
Unity vs testability
All this complexity has consequences for the code architecture. To our misfortune, high performance architectures usually work against good code design, a restriction you may also encounter in Unity. One of the core mechanisms that had to be designed in a special way, is the MonoBehaviours mechanism. If you ever wondered why the callbacks in MonoBehaviours aren’t implemented with interfaces or inheritance (as common sense perhaps suggests),
it is for performance reasons(See Lucas Meijer’s clarification in the comments). Without going into detail, this also works against the testability of MonoBehaviours. The fact that you can’t instantiate a MonoBehaviour with the new operator pretty much prohibits you from using any mocking frameworks out there. It probably wouldn’t be a good idea anyway with all the things that are going on behind the scene every time a MonoBehaviour is used. Intercepting this behaviour would generate lots of problems.
In the end it’s all about you, and how motivated you are to write testable code. Many approaches can solve the same problem but only few will work well for test automation. If you want to write testable code, sometimes you will need to write more code than you would think is necessary. If you are still learning (shouldn’t we be learning our whole life, anyway?) or just got on the test automation adventure path, you may find some of the code pieces or design assumptions as an unnecessary overhead. These, however, become a habit so quickly that you will not even notice when you start using the pro-automation designs without even thinking about it.
In this blog post, I promised to show you a way to design MonoBehaviour to be able to test them afterwards. It wasn’t completely true, because we won’t be testing MonoBehaviours themselves. You probably already have an idea of how to implement the Humble Object Pattern to your design to make it more testable but, nevertheless, let me show you the idea implemented in a real project.
Let’s create a use-case for the purpose of this example. Imagine a simple player controller that is responsible for steering a spaceship. To simplify the example, let’s put it in a 2D worldspace. We want the spaceship to be able to fly around in every direction. It has a gun that can shoot straight with bullets (space-rockets?) but not more frequently than a given firing rate. The number of bullets is also limited by the capacity of the bullet holder so once you shoot all of them you need to reload. To make it more interesting, let’s the make the movement speed dependent on the spaceship’s health.
A Monobehaviour that will serve as a controller for our spaceship could look like this:
In the FixedUpdate callback we read the input and perform the action depending on which buttons were pressed by the user. To move around the spaceship we need translate spaceship’s position with the speed constant according to the direction of the axes. As you can see in the code, the deltaX and deltaY variables are multiplications of: Time.fixedDeltaTime, the value from the input axis and the speed constant which itself is dependant on the health level.
On the Fire1 event (e.g. left mouse button click) we want to check if it is possible to shoot the bullet. In the first place, we need to have at least one bullet left in the bullet holder. Secondly, we want to only allow the spaceship to shoot at certain rate (once every half a second in this case). Therefore, we check how much time has passed since the last bullet was fired. If we’re good to go, we spawn the bullet.
The Fire2 event will simply reload the bullet holder.
To write unit tests for this logic, we need to overcome two problems. The first one, as previously mentioned, is the non-mockable MonoBehaviour class on which we depend on via inheritance. The second problem is more general for real-time software. Our logic is dependent on time (firing rate) which makes it impossible to perform unit tests since we can’t intercept the static Time class from Unity. The good news is that all this can be solved.
Let’s refactor our code a bit by applying some simple method extraction refactorization and keeping in mind that the logic methods should not reference the Unity API (Input handling and bullet instantiation in this case). The time dependency in the if statement, should be extracted to a separate method as well. The final result should look more or less like this:
As you can see, the FixedUpdate method here does nothing more than passing on the input from the users to the method that does the the logic part. The firing rate check was extracted to CanFire method, that generate the result “true” if a specified amount of time has passed. This extraction is important as it will allows us write unit tests later. If we were able mock the SpaceshipMotor class right now, we would simply intercept the CanFire method and make it return true or false whenever we intended to. It would make the test time-independent. But since we can’t mock SpaceshipMotor because it inherits from Monobehaviour, we need to apply the Humble Object Pattern.
How do we do that? We simply need to extract all the logic code that doesn’t use the Unity API to a separate class and introduce a reference to it to the SpaceshipMotor. Let’s look at the class again and see what to extract. The TranformPosition and InstanciateBullet use the Unity API but everything else can be extracted. I know there is also the static Time class but let me get back to that later.
The last thing left to explain before we do the actual extraction is how the extracted logic communicates with the Unity API without depending on it. This is the place where the interfaces come in. The class with logic will have a reference to an interface, and I will not care about the actual implementation. To keep things simple, we can implement those interfaces directly in MonoBehaviour itself! Let’s take a look at the following 2 classes:
Let’s start with the SpaceshipMotor class. The class implemented some interfaces that are responsible for transforming the position of out spaceship and instantiating the bullet respectively. The class itself got a field that refers to the SpaceshipController which implements all the logic now. The SpaceshipController class knows nothing about the SpaceshipMotor and the only thing it can do is it invoke methods from the interfaces it references.
Unity won’t serialize references to the interfaces. If you don’t care about serialization, simply pass the interface references while constructing the SpaceshipController class. Otherwise, you can set the references in OnEnable callback that is called every time after the serialization happens. Just for the record, the whole SpaceshipMotor class will be serialized in the usual way, it’s just the interface references that will be lost.
You must have noticed the Time class reference in SpaceshipMotor. I know I said there should be no Unity API reference in here but I left it there to demonstrate a different approach for handling time dependant dependencies. Ideally, we could simply pass the Time.time value as an argument to the methods.
For UML fans, this is the end result as a (simplified) UML diagram:
With the decoupled SpaceshipMotor class there’s nothing preventing us from writing some unit tests. Take a look at one of the tests:
The test validates that you can’t fire if you have no bullets left. The test itself is structured according to the Arrange-Act-Assert pattern. In the arrange part we create object mocks with GetGunMock and GetControllerMock methods. The GetControllerMock, besides creating a mock, overrides the behaviour of CanFire method to always return true. This removes the time dependency from the controller object. Next, we set the current bullet number to 0. After that, we apply fire to the controller class and we assert if Fire has not been called on the gun controller interface.
There are few more tests in the project. You can grab it from here and play with it bit. I used NSubstitute for the mocking object. We also ship a version of it with the Unity Test Tools. All of the three versions of the controller we discussed here are attached in the project.
That’s it from me today. I hope you enjoyed the read, and happy testing!
31 코멘트코멘트 구독
코멘트를 달 수 없습니다.