Unit Testing StrangeIoC Mediator

StrangeIoC is an IoC Framework for Unity3D. It makes heavy use of interfaces and therefore most of the code written with it is unit testable. But the views and the mediators are not.

That the view can’t be unit tested is clear.
But the mediators?

I want to write my mediators in a test-driven way. Thus I need to write unit tests for them.
So let’s have a look what restricts the unit tests.

  1. Mediator derives from MonoBehaviour
    MonoBehaviour can not be instantiated directly.
    It must be created by Unity3D, e.g. by calling GameObject.AddComponent<TMediator>()
  2. A Mediator has an association to a concrete view injected by StrangeIoC.
    This means the test will need an instance of the concrete view (and the view itself derives from MonoBehaviour too).

See the following example setting with which I will try to find a way around the problems.
There are some cities and streets connecting them. If the user clicks a city a truck is sent to the next city.

public class CityView : View
    {
        private readonly Signal _clickSignal = new Signal();

        public Signal ClickSignal
        {
            get { return _clickSignal; }
        }

        void OnMouseDown()
        {
            ClickSignal.Dispatch();
        }

        public void StartTruck(GameObject waypointRoot)
        {
            var truck = Instantiate(Resources.Load("Truck")) as GameObject;
            var splineController = truck.GetComponent<SplineController>();
            splineController.SplineRoot = waypointRoot;
            splineController.AutoStart = true;
            splineController.Start();
        }
    }
public class CityMediator : Mediator
    {
        [Inject]
        public CityView ConcreteCityView { set; get; }

        [Inject]
        public IStreetMap StreetMap { get; set; }

        public override void OnRegister()
        {
            base.OnRegister();

            ConcreteCityView.ClickSignal.AddListener(OnClick);
        }

        private void OnClick()
        {
            Debug.Log("City " + CityView.Name + " was clicked!");
            var streetView = StreetMap.Streets.FirstOrDefault(view => view.Cities.Contains(CityView));
            if (streetView != null)
            {
                ConcreteCityView.StartTruck(streetView.Waypoints);
            }
        }
    }

Unit tests inside Unity3D

As MonoBehaviour can not be instantiated directly (You would get this exception: SecurityException: ECall methods must be packaged into a system module), we must go inside Unity3D.

Luckily Unity released the Unity Test Tools, which can execute NUnit unit tests (and integration tests) inside unity (and also as batch on command line). After downloading the package from the asset store into the unity3d project, there is a new main menu entry “Unity Test Tools” from which unit tests can be run.

Problem 1 solved with the drawback that the tests can not be run inside Visual Studio. But well… better than nothing.

Getting rid of the concrete view

Now I can start writing the tests.
Here is a part of one which shall test, if a city is clicked, a truck is sent.

[TestFixture]
public class CityMediatorTest
{

    public CityMediator ObjectUnderTest { get; set; }
    public GameObject TestContainer { get; set; }
    private IStreetMap StreetMap { get; set; }
    private CityView CityView { get; set; }

    [SetUp]
    public void SetUp()
    {
        StreetMap = Substitute.For<IStreetMap>();

        // Create the GameObject and its MonoBehaviours
        TestContainer = new GameObject();
        TestContainer.AddComponent<CityMediator>();
        TestContainer.AddComponent<CityView>();

        // Get the MonoBehaviours from the GameObject
        CityView = TestContainer.GetComponent<CityView>();
        ObjectUnderTest = TestContainer.GetComponent<CityMediator>();

        // Wire up the object under test
        ObjectUnderTest.StreetMap = StreetMap;
        ObjectUnderTest.ConcreteCityView = CityView;

        ObjectUnderTest.OnRegister();
    }

    [Test]
    public void ClickSignal_StartsTruck()
    {
        // Arrange
        var streetView = Substitute.For<IStreetView>();
        streetView.Cities.Returns(new[] { ObjectUnderTest.CityView });
        StreetMap.Streets.Returns(new[] { streetView });
        // Setup more view things …

        // Act
        CityView.ClickSignal.Dispatch();

        // Assert
        ???
    }
}

You can see, that the view is instantiated by unity with the call AddComponent<CityView>(), then retrieved with GetComponent<CityView> and finally put into the CityMediator.
So the CityMediator uses the concrete View for the test.

But how to assert that the truck was sent?

CityMediator calls StartTruck() on CityView, which creates a new GameObject (a preconfigured game object from inside a resources folder of the asserts of the project) and calls some operations.

Maybe I can look for the existence of the game object inside unity to assert if the truck was sent?
But well… shall this test really depend on so much view specific behavior?
And do I really want to fully setup the view too, just to test the mediator, then I would have to change the test when the view changes inside?

Definitively NO! It shall be a unit test and not a big integration test.

But there is an easy workaround. Give the view an interface.
Just put all methods called by the mediator inside the interface.

public interface ICityView
{
    Signal ClickSignal { get; }
    void StartTruck(GameObject waypointRoot);
}

Now the Mediator shall also only depend on the interface of the view.
Unfortunately we can not map an interface of a view to a mediator at StrangeIoC (if someone knows a way, please let me know!) so, we have to use a small trick for that:

public class CityMediator : Mediator
    {
        [Inject]
        public CityView ConcreteCityView { set { CityView = value; } }

        public ICityView CityView { get; set; }

        [Inject]
        public IStreetMap StreetMap { get; set; }

        public override void OnRegister()
        {
            base.OnRegister();

            CityView.ClickSignal.AddListener(OnClick);
        }

        private void OnClick()
        {var streetView = StreetMap.Streets.FirstOrDefault(view => view.Cities.Contains(CityView));
            if (streetView != null)
            {
                CityView.StartTruck(streetView.Waypoints);
            }
        }
    }

Now we have got a the new  property CityView, which is used inside the mediator.
The former ConcreteCityView only has a setter so that we can not accidently use the concrete class inside the mediator.
Thus we can mock the View away inside our unit tests and StrangeIoC can give use the concrete view in productive environment.

See the updated test now

    [TestFixture]
    public class CityMediatorTest
    {
        private IStreetMap StreetMap { get; set; }

        private ICityView CityView { get; set; }

        public CityMediator ObjectUnderTest { get; set; }

        public GameObject TestContainer { get; set; }

        [SetUp]
        public void SetUp()
        {
            CityView = Substitute.For<ICityView>();
            StreetMap = Substitute.For<IStreetMap>();

            TestContainer = new GameObject();
            TestContainer.AddComponent<CityMediator>();

            ObjectUnderTest = TestContainer.GetComponent<CityMediator>();
            ObjectUnderTest.StreetMap = StreetMap;
            ObjectUnderTest.CityView = CityView;
            ObjectUnderTest.CityView.ClickSignal.Returns(new Signal());
        }

        [Test]
        public void ClickSignal_StartsTruck()
        {
            var streetView = Substitute.For<IStreetView>();
            streetView.Cities.Returns(new[] {ObjectUnderTest.CityView});
            StreetMap.Streets.Returns(new[] { streetView });

            ObjectUnderTest.OnRegister();
            CityView.ClickSignal.Dispatch();

            CityView.Received().StartTruck(Arg.Any<GameObject>());
        }

        [Test]
        public void ClickSignal_WillNotStartTruckIfNoStreetIsFound()
        {
            ObjectUnderTest.OnRegister();
            CityView.ClickSignal.Dispatch();

            CityView.DidNotReceive().StartTruck(Arg.Any<GameObject>());
        }
    }

We can assert that the truck was sent by simply asking the mock, if it received a call or not.

Problem 2 solved too.

Advertisements