Practical Unit Testing in Unity3D

Kuldeep Singh
XRPractices
Published in
7 min readNov 17, 2019

The definition of a unit of work is vague in software development, so is the boundary of the unit testing. Most of the time, I have seen developers writing integration tests on the name of the unit tests, in other words, writing unit tests which mostly test the functionality written in the 3rd party libraries and platforms. It results in a complex and long-running test suite, lengthy CI builds, and eventually, we start to avoid running tests in each builds as they kill a lot of productive dev hours.

Unity3D tests are also of a kind of integration test where we test the complete life cycle of a game object, refer the below post on basics setup for unit testing in Unity. We still can follow some guiding principles to restrict the boundaries of unit tests.

Principles of Unit Testing

Follow the below principles to avoid getting into the trap of unit tests vs integration tests vs functional automation tests.

  1. 15ms unit test — A unit test must run within 15 milliseconds. anything beyond it needs to rethink.
  2. Test what you code — Write tests only for the code that you write.
  3. Mock what others code — Mock everything else, by injecting proxy implementations. Refer to mock test frameworks such as NSubstitute, Moq, and FIE.
  4. Categorize the tests — above rules fits well when we need to test business logic, however, when it comes to game object’s behavior it becomes difficult to injects proxy implementations, and game object runs in play mode which means more time to run them, so in that case it better to categories the tests as integration and functional automation tests so that we can run them separately.

Let's build a project unity project ShowMeTDD where we will follow TDD and the above principles.

ShowMeTDD Project Setup

This project is about creating a simple application to sum two given numbers and show them on the screen.

  1. Create InputFields(“InputFieldA”, “InputFieldB”) and a Plus Button(“A+B”) and Text (“Result”) in the scene.
  2. Add a script Controller.cs to Plus Button.
Controller.cs

3. Add method ButtonClickHandler() to Plus Button’s onClick and drag the respective game objects.

We have done some work without thinking of tests. Fine, we haven’t written any meaningful code yet, However, whatever we have done so far is also testable but as a functional test which we will cover later.

Setup Assemblies

  1. Create a project assembly in Scripts/ShowMeTDD.asm with default settings
  2. Editor Mode Tests Assembly in a Folder Tests/EditorTests/EditorTests.asm add assembly reference to ShowMeTDD so that classes in main projects are accessible in the test assembly.

From here we will not write any code without thinking a test in mind. Let’s start —

Test-Driven Development

Follow the TDD practices described earlier. Write your first favorite failing test.

  1. The First Failing Test — ClickHandlerShouldSetResultTo0IfNoValuePassed and Setup. Setup is very important in unity. All the MonoBehaviour Unity Objects should be initialized via gameObject.AddComponent<ClassName>(). So that they follow the MonoBehaviour life cycle, and available in the root game object.

2. Make it Green — to make it green we need to assign all the required elements in the Controller object, but they are private and serialized. Here are the ways to set the private fields. There are multiple options here but have pros and cons.

ControllerTest.js

Option A — Injecting Private Fields using reflection.

Pros — no need to change the encapsulation of the main class.

Controller.js

Cons - Reflection is slow and error-prone. It is a way to forcefully breaking the encapsulation.

Option B — Add Setters in the Controller for required fields and set the values in Test Setup.

Pros — Faster.

Cons — Need to break the encapsulation and class design just for the test cases. All the private fields get exposed.

ControllerTest.cs

Option C — Have a Start method and find and assign objects there. But to call the start method of MonoBehaviour, we need to make the test as play mode test. So change the test assembly to allow “All Platform”, and make the test [UnityTest]

Controller.cs

Pros — Safe, follow OOP principles, and no need to pass fields even from the Game hierarchy.

Cons — Makes the tests slow, as they need to run in play mode. Error-prone, it depends on the name of the name objects or on object hierarchy. It makes the Start method slower.

Each of the options has pros and cons. Keeping the first principle of Unit testing in mind, I prefer Option A.

IService.cs
ControllerTest.cs

3. Continue writing and refactoring the tests, and we have our next failing test as ClickHandlerShouldCallServiceAndSetResultIfValidValuesArePassed. Let's create an IService interface, and call it in the

ButtonClickHandler().

Controller.cs

Here we need to mock testing.

Mock Testing

Lets set up mock testing framework NSubstitute. Follow one of below steps:

a) Download NSubstitute and build it in Visual Studio, place the dlls in build folder in /Tests folders.

b) Download the NSubstritute unity package from here and import it in unity.

c) Download NuGet in IDE — For Rider Tools>NuGet>Manage NuGet Packages for Solutions and Search for NSubstritute and then click “+” to install.

On a successful install, you should be able to do “using NSubstitute” in the test class. Follow the NSubstitute documentations https://nsubstitute.github.io/help/getting-started/

Mocking the service — let's pass the last test by mocking the service. Let’s substitute a service. Substritue.For<ClassName>

ControllerTest.cs
Controller.cs

Please note that you can mock a method only if it is public virtual, protected virtual, protected internal virtual, or internal virtual with InternalsVisibleTo.

Mock a method — mock the return value

mock.Method(parms..).Returns(value)

Verity the service method invocation — verify the number of times the proxy method is invoked. mock.Received(numberOfTimes).Method(Params..)

With this, we can be sure that functionality till the controller is correct and fully covered. Now we can implement the Service class following TDD.

Continue TDD

and implement the service logic independently of the unity.

CalculatorServiceTest.cs
CalculatorService.cs
Controller.cs

Now add assign calculator in the Start() of Controller.

This completes the editor mode tests

And now you may confidently play the application.

Source code @ https://github.com/thinkuldeep/ShowMeTDD

Conclusion

We have discussed practical aspects of unit testing in Unity and tried to distinguish it from integration or functional automation tests. In the next post, we will cover what really the integration test or functional automation tests are, and how to categorize the tests, also the complex cases like update, fixedUpdate etc.

Look at the following articles :

--

--

Kuldeep Singh
XRPractices

Engineering Director and Head of XR Practice @ ThoughtWorks India.