Search Unity

Question How to test class that uses a static custom messaging system

Discussion in 'Testing & Automation' started by Giovarco, Sep 3, 2023.

  1. Giovarco

    Giovarco

    Joined:
    Mar 10, 2016
    Posts:
    45
    Hello!

    I have the following class I want to test:
    Code (CSharp):
    1. namespace Assets.Scripts.Global.ShipCommands
    2. {
    3.     public class RequestInteractionEventHandler : MonoBehaviour
    4.     {
    5.         private ShipSelectionModel selectionManager;
    6.         private ActionBarModel actionBarModel;
    7.         private RequestScanComponent requestScanComponent;
    8.         private RequestCommandoDispatchComponent requestCommandoDispatchComponent;
    9.         private RequestGenericInteractionComponent genericInteractionComponent;
    10.         private RequestDriveAwayComponent requestDriveAwayComponent;
    11.  
    12.         private void OnEnable()
    13.         {
    14.             MessageSystem.Instance.AddListener<RequestInteractionEvent>(HandleInteractionEvent);
    15.         }
    16.  
    17.         private void OnDisable()
    18.         {
    19.             MessageSystem.Instance.RemoveListener<RequestInteractionEvent>(HandleInteractionEvent);
    20.         }
    21.  
    22.         private void Start()
    23.         {
    24.             // (Initializing private fields...)
    25.         }
    26.  
    27.         private void HandleInteractionEvent(RequestInteractionEvent interactionEvent)
    28.         {
    29.             // (...)
    30.         }
    31.  
    32.     }
    33. }
    I'm not new to testing in general: I write unit and integration tests almost daily; but I've never done it in Unity (and NUnit and NSubstitute).

    I'd like to test that during the execution of HandleInteractionEvent, RequestCommandoDispatchComponent is called. The logic doesn't really matter. The question is: how can I test this case since I'm using my own MessageSystem?

    My expectation is that I have to send a message in my test MessageSystem.Instance.Send(requestInteractionEvent) but then I guess, the private method HandleInteractionEvent is not called because OnEnable() is also not called? I'm a bit confused about how to solve this. Welp! :/
     
  2. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,652
    Because your class is a MonoBehaviour, then in order to create an instance of it in the test, you are going to have to attach it to a GameObject (e.g. using the AddComponent method). That should cause OnEnable to be called automatically.

    That said I think the way I'd solve this would probably be to make HandleInteractionEvent be 'internal' visibility, not 'private', and then call it directly from the test. You shouldn't exercise the MessageSystem if you don't have to.
     
  3. Giovarco

    Giovarco

    Joined:
    Mar 10, 2016
    Posts:
    45
    This "internal" approach doesn't work for me apparently. I think that this is because my test is in /Editor? I'm not familiar with assembly definitions and just didn't want to deal with them.

    The approach of attaching the script to the GameObject makes sense. However, for example, how can I mock dependency ShipSelectionModel selectionManager? An example of code would be appreciated.
     
  4. Giovarco

    Giovarco

    Joined:
    Mar 10, 2016
    Posts:
    45
    I'm still trying to make it work but without success... I imported NSubtitute through NuGet from Unity, then I added the DLL to the assembly definition asset. I get The type or namespace name 'For' does not exist in the namespace 'NSubstitute' (are you missing an assembly reference?)
     
  5. Giovarco

    Giovarco

    Joined:
    Mar 10, 2016
    Posts:
    45
    Screenshot Capture.PNG
     
  6. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,652
    By default "internal" means that the symbol is accessible to any other code in the same assembly; your tests are (or should be) in a separate assembly so it initially won't do anything. But by adding the
    [assembly:InternalsVisibleTo("NameOfTestsAssembly")]
    attribute to your assembly, you can extend that access to your tests. This is one of the main use cases that 'internal' accessibility and the InternalsVisibleTo attribute were designed for.
     
  7. Giovarco

    Giovarco

    Joined:
    Mar 10, 2016
    Posts:
    45
    But this doesn't explain why NSubstitute isn't detected, does it?
     
  8. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,652
    I have no idea about NSubstitute, sorry - I've only used Moq.
     
  9. sbergen

    sbergen

    Joined:
    Jan 12, 2015
    Posts:
    53
    It is
    Substitute.For<T>()
    , not
    NSubstitute.For<T>()
    .
     
  10. Giovarco

    Giovarco

    Joined:
    Mar 10, 2016
    Posts:
    45
    I can't believe I actually made that mistake, thanks :oops:

    By any chance you know how to fix the assembly definition issue or maybe provide documentation about how to make a global assembly definition so that all tests can access all classes?
     
  11. Giovarco

    Giovarco

    Joined:
    Mar 10, 2016
    Posts:
    45
    You mean Moq + Unity? I don't find much about this combination.
     
  12. sbergen

    sbergen

    Joined:
    Jan 12, 2015
    Posts:
    53
    You can add an assembly definition in the root of the project, which will include all code in all directories not including an assembly definition. The biggest complication is that you'll have to add assembly definitions for all `Editor` directories (or move them all into one directory, which has an assembly definition).