Automated Tests Can Save Your Code
Unity has provided a testing framework since 2017. Unfortunately, Unity related projects do not utilise tests as much as expected, whether from the asset store, Github or even Unity’s packages. Perhaps, this is due to challenges involved in writing tests in a game development context. I would like to share what these challenges are, our philosophy on tests in Mighty Bear Games and a sample test to start you off with.
One of the main challenges that results in inertia to write tests is the additional time it takes. It may take 10 minutes to write tests for a feature that took the same amount of time to implement. That’s double the development time!
However, the benefit of writing tests is the time saved down the line. The 10 minutes can save hours in the future debugging and fixing bugs. While it looks impressive to finish a task quickly, projects are more likely to derail from tasks that are completed quickly, but create indeterminate bugs with unpredictable time to fix than tasks that take longer to complete but with a more expected timeline.
Another resistance to writing tests is the fact that the costs are upfront. In order to write tests, one needs to spend time to ensure that the code does not have too many dependencies. Otherwise, it’s hard to set up the tests and the result would be something that itself is entangled and fragile.
TDD to the rescue?
Can TDD come to the rescue? One of the benefits of Test Driven Development (TDD) is that it allows you to write the tests first and then code later. This process forces you think about good code design. If you’re used to the mentality of “Let’s make the code work first and if I have time, I’ll refactor it”, it can be unnerving to lose that time flexibility. The fact is, it is also unnerving to refactor code that already works. You would not know whether refactoring would break the code, and that is because there are no tests. One unfortunately lands themselves in a vicious cycle.
For all its benefits, TDD is, however, not a magic bullet. It is still possible to write bad code and bad tests. Why so? It takes time to learn how to write good tests. You have to invest time and effort in writing good test, but like all investments, not all of them provide good returns. Bad tests cost time to write and maintain and does not provide much value.
While there are many that advocate going all-in on TDD, if you are new to writing tests, I would suggest considering starting off with simpler tests that may give you good value for your time. Gradually add different types of tests as you gain clearer understanding of what constitutes good tests.
Should tests be written for prototype code?
It seems pointless to write tests when developing a prototype when a lot of the design and code is subject to change. For code that is written only for investigative purposes to find out how certain libraries work and thus would be discarded, I agree it would be superfluous.
However, there is prototype code that can be reused as part of a quick prototyping framework where tests would be highly useful. It is also highly likely that some of the prototyping code would evolve into production code. If you are already adept at writing tests, a good starting point would be writing tests for prototyping code that would evolve into production code.
So how do I get started?
One of the tests to I would recommend starting with are data validation tests. An example would be to find out if any text in the game has not been localized. In Mighty Bear, if a text requires localization, the TextMeshProUGUI script would have a corresponding Localize script which handles the localization. If not, we explicitly add a NotLocalizedTextUI script. The test would fail if it has neither scripts.
Notice that I also added a check for PrototypeTextUI that would allow the test to pass. Once in production, that check would be removed, causing the test to fail. This helps us clean up any prototype text that was added to the game that should be removed in production.
For completeness sake, here are the test methods where the check is being called. The first test checks that all prefabs are localized. The second checks that all game objects in all scenes in the build are localized.
Another example of a useful data validation test is to check that the variables modified by artists/designers are correctly set. One way you could do that is by tagging the variable using NotNull attribute.
[SerializeField, NotNull] private VideoPlayer videoPlayer;
Try implementing a test for that or feel free to share a different approach in the comments below!
Testing data saves a lot of time figuring out what went wrong. It detects any wrong configurations or invalid data due to human error. The future you will thank you!
Though writing tests in Unity is a good first step in, it is slightly different from testing code itself. That’s where I’d like to go further in depth into in future articles, so stay tuned…