Where to focus automated tests, and how to architect for them - Coming from corporate to game dev

I’m laying down the tech design and establishing the patterns for a fairly broad piece of software. Not a game per se, rather a unity-based dev-kit/framework to be given to customers to create their own games. Lots of moving pieces, 2d/vr players, movement & interaction systems, UIs, networking, etc.

I’m trying to gauge the importance of unit, integration, and end-to-end tests, and how much the architecture of the production code should be designed to accommodate for these, but I’m struggling to translate the competing viewpoints from conventional software engineering into the game dev / unity domain. My only other job was at a big bank, obviously tests to the moon and back, CI/CD pipelines etc, obviously we don’t need anything near that extreme, but I’m trying to gauge where the line is.

The conventional wisdom is “testing pyramid, write a load of unit tests, and a few e2e tests, architect things using DI so you can properly mock the dependencies and isolate the system under test”. I’m running into two problems with this

Problem #1

In certain cases this makes my prod code needlessly convoluted. Here’s my class before making it unit-test friendly

public class PlayerController2D : PlayerController
    {
        [SerializeField] private Camera _camera2D;
        [SerializeField] private Player2DLocomotor _playerLocomotor2D;
        [SerializeField] private Interactor2D _interactor2D;
        [SerializeField] private CharacterController _characterController;
        private Player2DControlConfig _controlConfig;

        //substitute for constructor
        public void Initialize(Player2DControlConfig controlConfig)
        {
            _controlConfig = controlConfig;
            _interactor2D.Initialize(_camera2D);
        }

        //Logic stuff and things...

Some higher-level service is instantiating my player2d, and injecting its control config (mouse sensitivity etc) with what it gets from the network. We have four other dependencies which aren’t injected by the “constructor” (initialize method, in this case). This current approach does in a way fit DI principles, as they’re in a way being “injected” through the inspector… but not in a way that’s really testable. So we refactor to inject all dependencies…

Now we’ve got a factory class that looks like this

    public class PlayerController2DFactory
    {
        public static PlayerController2DFactory _instance;

        //Lazy init to mock the factory with mock dependencies for higher level integration tests
        public static PlayerController2DFactory Instance {
            get {
                _instance ??= new PlayerController2DFactory();
                return _instance;
            }
        }

        public static PlayerController2D Create()
        {
            GameObject player2DPrefab = Resources.Load("2dPlayer") as GameObject;
            GameObject instantiated2DPlayer = GameObject.Instantiate(player2DPrefab, null, false);

            PlayerController2D playerController2D = instantiated2DPlayer.GetComponent<PlayerController2D>();

            Player2DControlConfig controlConfig = //however we get this

            //Find components from the instantiated GO heirarchy
            Camera camera2d = instantiated2DPlayer.GetComponentInChildren<Camera>();
            Player2DLocomotor player2DLocomotor = instantiated2DPlayer.GetComponentInChildren< Player2DLocomotor>();
            Interactor2D interactor2D = instantiated2DPlayer.GetComponentInChildren<Interactor2D>();
            CharacterController characterController2D = instantiated2DPlayer.GetComponentInChildren<CharacterController>();
            
            playerController2D.Initialize(controlConfig, camera2d, player2DLocomotor, interactor2D, characterController2D);
            return playerController2D;
        }
    }

And a player class that looks like this

    public class PlayerController2D : PlayerController
    {
        private Camera _camera2D;
        private Player2DLocomotor _playerLocomotor2D;
        private Interactor2D _interactor2D;
        private CharacterController _characterController;
        private Player2DControlConfig _controlConfig;

        public void Initialize(Player2DControlConfig controlConfig, Camera camera2D, Player2DLocomotor player2DLocomotor, 
            Interactor2D interactor2D, CharacterController characterController)
        {
            _controlConfig = controlConfig;
            _camera2D = camera2D;
            _playerLocomotor2D = player2DLocomotor;
            _interactor2D = interactor2D;
            _characterController = characterController;

            _interactor2D.Initialize(_camera2D);
        }

We’ve added a chunk of wiring, and we aren’t really getting any benefits of DI, since we kinda already had DI before. This seems like a really whack pattern to be adopting, instantiating the player and injecting its controller with a component from its own hierarchy, rather than just allowing the controller to manage the things in its hierarchy itself, really? Feels like we’re trying to force our way against Unity’s fundamentals (component system, DI through serialize fields) rather than actually utilising them.

In fact, if you keep applying this pattern down through the layers, you’d have another factory to create the locomotor script, wiring in the transform and/or the character controller. Maybe rather than GetComponent in the factory, you do AddComponent and set that component up programmatically… hell, maybe you don’t instantiate a prefab at all, maybe you programmatically create an empty gameobject and add/configure its components all in the code. At this point, we’re just doing all the work the unity editor GUI makes easy in our scripts instead. There is some benefit to decoupling from Unity, easier to port to other engines etc… but we’re still dealing in terms of Unity’s objects, Cameras, CharacterControllers, Transform etc. We could abstract all of those behind adapter interfaces, but, ah, we’ve entered over-engineering hell, haven’t we. The line has got to be drawn somewhere, and we’ve definitely blown far past it.

Anyway, back to our code snippet above, at least now things are properly isolated for unit tests on our player class, e.g we can inject a mock of the Interactor and confirm that certain methods on that component are called under the appropriate conditions

Problem #2

I’m seeing a lot of pretty valid arguments against the conventional testing pyramid and against unit tests. People are saying that tests should protect functional requirements, not the implementation. Ideally, you want to be able to refactor the implementation without breaking the tests, as long as the requirement is still fulfilled. That way, when you refactor, and the test goes red, you know it’s because you’ve broken functionality rather than "well I changed the prod code so of course the test broke).

For example, if I have a public function on my player to teleport it about the scene, my tests shouldn’t care that works by invoking a method on the transform, or the character controller, that’s an implementation detail, I just want my test to confirm that the player gets to where its going, without breaking anything along the way.

Testing at the method level or even class level, especially when you’re injecting a bunch of specific mocks into the class, means your tests end up pretty hard-coupled to the implementation (and it seems that you often end up actually testing the mocks as much as the sut itself)

In the domain of game dev, it seems the core things you want to test are shifted even more towards e2e/integration, you’re looking at how input, UI, physics, networking etc all orchestrate together at runtime, more so than isolated chunks of logic at the method or even class level.

So basically, I’m trying to gauge if I should be laying all these architectural patterns down for the production code, when there’s decent arguments against reaping the benefits that this gives us in the first place

All that said - I’d love to hear anyone’s insight. How many tests do you teams write, and at what levels? How closely do you follow TDD? How do you balance the trade-offs of added architectural complexity and overhead with testability? How do you find testing in game-dev follows or differs from conventional software?

Cheers!

I find myself wondering about automated tests from time to time. On the one hand I hear comments like “about 15 - 50 errors per 1000 lines of delivered code” or “spending 75% of time debugging”, which makes automated tests sound great. On the other hand games change so rapidly that it’s a real pain having to update tests from refactoring or even just throwing it all away when features are dropped.

There definitely seems to be a lack of automated testing in video games. I feel like most testing just gets pushed onto the QA team. I’d be interested in seeing some stats about it, I wouldn’t be surprised if less than 5% of games had any kind of automated tests.

Personally, I don’t really bother testing MonoBehaviours at all. I normally only worry about things I can test with standard constructor dependency injection like services, view models etc. I also don’t do that many tests, I mainly like to know that my classes are testable and if I find a bug then I can write a unit test for it. If I was releasing a package or releasing something on Github for others to use I’d probably include more tests.

I also only focus on unit tests and I might occasionally do some integration tests. I’ve seen a few Unity game studios using AltTester but I’m not really a fan of it.