Testability is an important feature of any software product – game development is not an exception. To enable testability, all the components should be independent and testable in isolation.

When we want to test something in isolation it means that we want to decouple it. Loose coupling is what we need. It is so easy to embed hidden dependencies into your game and it is so hard to break them. This article will help you understand loose coupling and dependency injection within a project in Unity, using the example project on github.

Lets take handling input as an example.


public class SpaceshipMotor : MonoBehaviour
{
  void MoveHorizontally ()
  {
    var horizontal = Input.GetAxis ("Horizontal");
    // ...
  }
}

MoveHorizontally method uses static Unity API (Input class) without telling you. It considers this call to be his private business and you can’t control or influence the situation. It makes SpaceshipMotor class tightly coupled with Unity static API, and you can’t verify the behaviour of the SpaceshipMotor class unless you physically press the key on the keyboard. It’s annoying.

Now lets take this situation under control. You are in charge here.

The SpaceshipMotor class is using only horizontal axis, so we can define a short description what kind of functionality it expects from user input.


public interface IUserInputProxy
{
  float GetAxis(string axisName);
}

Then you can substitute the call to real Input with the call to our abstraction.


public class SpaceshipMotor : MonoBehaviour
{
  public IUserInputProxy UserInputProxy {get;set;}

  void MoveHorizontally ()
  {
    var horizontal = UserInputProxy.GetAxis (“Horizontal”);
    // …
  }
}

Now you are in charge of the situation! The class can’t operate unless you provide it IUserInputProxy implementation.

This is called Dependency Injection (DI). When the dependency (Input in our case) is passed to the dependant object(SpaceshipMotor class) and it becomes part of it’s state (a field in our case).

There are several options of passing a dependency: constructor injection, property injection, method injection.

Constructor injection is considered to be the most popular and the most robust approach as when the dependency is passed in the construction phase our chances to have object in uninitialized state is minimal.

public class SpaceshipMotor : MonoBehaviour
{
  private readonly IUserInputProxy userInputProxy;

  public SpaceshipMotor (IUserInputProxy userInputProxy)
  {
    this.userInputProxy = userInputProxy;
  }
}

But Unity engine is calling the constructors for MonoBehaviours and we can’t control this process.

 Still, property and method injection are both usable in this case.

 The easiest approach for manual Dependency Injection (DI) would be to use the script that will inject the dependencies.

In “Growing Games Guided by Tests” we are using an interface to expose property dependency.


public interface IRequireUserInput
{
  IUserInputProxy InputProxy { get; set;}
}

And a script that allows us to set the parameters of fake input in the scene and inject it when the tests start.


public class ArrangeFakeUserInput : MonoBehaviour
{
  public GameObject Spaceship;
  public FakeUserInput FakeInput;

  void Start () {
    var components = Spaceship.GetComponentsMonoBehaviour ();
    var dependents = components.Where(c=c is IRequireUserInput)
              .CastIRequireUserInput();
    foreach(var dependent in dependents)
      dependents.InputProxy = FakeInput;
  }
}

How does this contribute to testability?

 

We have lots of examples in “Growing Games Guided by Tests” where fake user input is injected with helper script and it lets us test the behaviour.

On the other hand we can write unit tests for classes that depend on abstractions.


[Test]
public void ChangesStateToIsFiringOnFire1ButtnPressed()
{
  // Arrange
  // Setting test double for user input
  IUserInputProxy userInput = Substitute.ForIUserInputProxy ();
  // Telling GetButton method of test double to return true
  // if state of “Fire1” was requested
  userInput.GetButton(Arg.Is("Fire1")).Returns (true);
  // Passing the dependency to Gun object on creation
  Gun gun = new Gun(userInput);
  // Act
  gun.ProcessInput ();
  // Assert
  Assert.That(gun.IsFiring, Is.True);
  }

Now you see that there is no magic to dependency injection. It is the process of substitution of concrete dependencies with the abstractions and making them external to the dependant object.

To use DI on a large scale you need a tool to automate it . This will be the topic for our next blogpost.

 

پاسخ دهید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *