Search Unity

Question Dependency Injection - The right way?

Discussion in 'Testing & Automation' started by Dextozz, Nov 20, 2022.

  1. Dextozz

    Dextozz

    Joined:
    Apr 8, 2018
    Posts:
    493
    Okay, so I've been working with Unity Unit Testing Package for about two years now, on and off; and after all this time, I still don't know the best way of injecting dependencies in MonoBehaviour classes that would have them referenced in the inspector...

    Example class:
    Code (CSharp):
    1. public class TestMe : MonoBehaviour
    2. {
    3.    [SerializeField] private MyOtherClass someClass;
    4.    [SerializeField] private GameObject someObject;
    5.    [SerializeField] private int someInt;
    6.  
    7.    public void Awake()
    8.    {
    9.       // Do some Init stuff based on passed-in variables
    10.    }
    11. }
    Now... there's a few ways to go about this...
    1. Creating testing-only prefabs and loading those on a test-by-test basis
      Pros:
      - Nothing to worry about. Setup a prefab as you want and you're done
      Cons:
      - Creating a bunch of semi-useless content in the project. Just cluttering stuff up
      - Some tests might require a large amount of prefabs, which might be tedious to setup
      - Prefabs are very fragile. Any major change in the design of the code requires additional work to fix these as well
    2. Using Reflection to inject proper objects at runtime
      Pros:
      - Don't have to maintain a bunch of prefabs in the project for testing
      - Relatively simple and flexible
      Cons:
      - Very very fragile. Renaming variables breaks tests - IDE can help out, but I don't want to rely on that as not everyone uses Rider
    3. Using a DI Framework (ex. Zenject)
      Pros:
      - Kind of does all the heavy lifting for you
      Cons:
      - Introducing a third-party dependency in your project (I'd like to avoid this as much as possible)
      - Zenject is just horrid in my opinion. It's so heavy and "magical" that I can't accept using it
    Some more words about this: If you say that DI should be done by just taking the private fields and making the constructor arguments all the way up to a root object - I don't think it's possible. I've tried it multiple times, and there's no way to efficiently all the dependencies between these simple objects and a huge data class that injects data in your entire game.
     
  2. Lekret

    Lekret

    Joined:
    Sep 10, 2020
    Posts:
    359
    In my job project we simply don't test MonoBehaviours.
    We use ECS (Entitas) for core logic which allows access to anything which is ideal for tests. MonoBehaviours are view functionality which should not be tested.
    Or you can simply use public instead of SerializeField with encapsulation by naming or by interfaces with exposing properties only. I would rather do that instead of creating separate prefabs.
     
  3. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    I would advise to focus functional testing on non-MonoBehaviour classes and extract as much logic as possible away from Unity. Then do content validation on prefabs and other assets.

    At least in my experience, this is what's left if you split the two: plain C# classes contain all the if-statements, whereas MB usually just configures content (e.g. animation delays) and sets up references. You can write validation tests for production content and make assumption like "no missing inspector fields" or "sensible value ranges", but there shouldn't be much more interaction left, if you also extract everything else into unit-testable classes.

    As for integration or end-to-end tests: that's a debatable topic, but I believe, if you do high-level tests they should only be done against actual production content, otherwise you have double maintenance overhead and it's unlikely that the benefit is high enough to warrant this, if you already have good unit tests and content validation.
     
    sbergen and Lekret like this.