Over the weekend I added unit tests to the MonoGame.Extended project. Setting up the build server was easy enough. Then I went to write the first test for the camera class I wrote the other day, that's when I ran into troubles.

Imagine you're sitting down to write a unit test on a class that needs access to the GraphicsDevice. It might look something like this:

[Test]
public void Camera2D_LookAt_Test()
{
    var graphicsDevice = new GraphicsDevice(GraphicsAdapter.DefaultAdapter,
        GraphicsProfile.Reach, new PresentationParameters())
    {
        Viewport = new Viewport(0, 0, 800, 480)
    };
    var camera = new Camera2D(graphicsDevice);
    
    camera.LookAt(new Vector2(100, 200));

    Assert.AreEqual(new Vector2(-300, -40), camera.Position);
}

It doesn't really matter what this test is trying to prove, we are just trying to setup the dependencies of our Camera2D class so we can run the test. Let's see what happens when we do.

System.NullReferenceException : Object reference not set to an instance of an object.

What's going on here? The null reference exception is being thrown in the constructor of the GraphicsDevice. If we look a little deeper we'll find that MonoGame is quite a complex beast and there are many things that could go wrong. In our case, it's probably the call to Game.Instance.Window inside the PlatformSetup method. The actual cause doesn't really matter, the point is that GraphicsDevice secretly depends on Game and that's a big no no when it comes to unit testing.

So what can we do about it

Let's take a step back and think about the design of our Camera2D class. The real reason we inject a GraphicsDevice is because the camera needs access to the Viewport to get the Width and Height of the viewing area. We could just inject the Viewport directly, but unfortunately the Viewport type is a struct and is passed by value. That means if he viewport changes size (and it most likely will) the Camera2D will be in an invalid state.

To properly inject the Viewport and monitor it for changes we need to inject a reference to the class that has the Viewport property. This happens to be the GraphicsDevice. So now we understand how we got into this mess in the first place.

Of course, injecting a GraphicsDevice has it's share of problems beyond what we've seen so far. It carries a lot of baggage that the Camera2D class is not interested in. What we really need here is a role interface that defines exactly what the Camera2D class needs. Something like this:

public interface IViewportAdapter
{
    Viewport Viewport { get; }
}

Injecting something like this makes unit testing a breeze, because we've minimized the dependencies down to what they actually are. We could use NSubtitute to mock out our test data and recreate our test like this:

[Test]
public void Camera2D_LookAt_Test()
{
    var viewportAdapter = Substitute.For<IViewportAdapter>();
    viewportAdapter.Viewport.Returns(new Viewport(0, 0, 800, 480));
    var camera = new Camera2D(viewportAdapter);
    
    camera.LookAt(new Vector2(100, 200));

    Assert.AreEqual(new Vector2(-300, -40), camera.Position);
}

We've removed the dependency on the GraphicsDevice and our test passes. Problem solved, right? Not quite.

The downside of all this

To make our code testable we've had to pay a cost. Something has to implement our role interface to expose the Viewport property from the GraphicsDevice. Ideally the GraphicsDevice class would implement a few role interfaces itself, but unfortunately we don't have the luxury of changing MonoGame's code directly.

This means we've had to introduce another class in the middle using the adapter pattern.

public class ViewportAdapter : IViewportAdapter
{
    private readonly GraphicsDevice _graphicsDevice;

    public ViewportAdapter(GraphicsDevice graphicsDevice)
    {
        _graphicsDevice = graphicsDevice;
    }

    public Viewport Viewport
    {
        get { return _graphicsDevice.Viewport; }
    }
}

This muddies up an otherwise nice API, making it more confusing for people that have to use it. Now they need to create a ViewportAdpater before creating a Camera2D without a real reason to do so. We've changed our nice implementation to aid testing, which sux.

To improve this situation slightly we can introduce another constructor on the Camera2D class that takes a GraphicsDevice like this:

public Camera2D(GraphicsDevice graphicsDevice)
    : this(new ViewportAdapter(graphicsDevice))
{
}

None of this code is testable, so it sure better be kept simple.

There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies and the other way is to make it so complicated that there are no obvious deficiencies.

-- Charles Antony Richard Hoare

The good news is, once we've broken away from the seam that connects our code to MonoGame via the GraphicsDevice our code should become easier to test without all this fuss in the middle.

Conclusion

Look, I'm no TDD nazi but I understand the benefits of having tests. Especially for code that's particularly complex, likely to break or difficult to test manually. It's nice to know that you only have to test those annoying edge cases once, and let the test prove that you haven't broken it long into the future (hopefully).

Unfortunately, writing testable code is not always easy. Often it's a chore setting up all those dependencies and mocking out the test data. It's even more difficult, nearly impossible, if those dependencies haven't been written with testability in mind.

I don't blame the @MonoGameTeam either. They are just re-implementing the design of XNA after all, and Microsoft XNA was first announced in 2004. Unit testing was probably barely even a thing back then.

It would be nice if MonoGame implemented a role interface here and there, but for the moment it is what it is.