Entitas icon indicating copy to clipboard operation
Entitas copied to clipboard

How to deal with methods only running in the Unity scope?

Open obviouslynotthedarklord opened this issue 3 years ago • 9 comments

Hi, I have a have a question. I set up my Unity project and another solution for tests. I want to test my initialize system which makes use of the method UnityEngine.Random.Range.

When running the unit test in my xUnit project the test fails with the following exception

System.Security.SecurityException ECall methods must be packaged into a system module. at UnityEngine.Random.Range(Int32 minInclusive, Int32 maxExclusive)

I don't want to modify the Unity project because the code works fine...

How did you solve this?

obviouslynotthedarklord avatar Dec 31 '20 15:12 obviouslynotthedarklord

Hello, I'd wrap Random calls into RandomService:IRandomService, and then mock IRandomService with NSubstitute during tests

c0ffeeartc avatar Jan 01 '21 05:01 c0ffeeartc

The only option you have is to substitue/mock the logic. Random (https://github.com/Unity-Technologies/UnityCsReference/blob/61f92bd79ae862c4465d35270f9d1d57befd1761/Runtime/Export/Random/Random.bindings.cs) is part of the Untiy C++ Engine and can't be accesses outside of it. To see which parts can be accessed if you link the UnityEngine.dll see the Repo of Unity: https://github.com/Unity-Technologies/UnityCsReference Everything that has no extern call can be used (eg implemented directly in c#).

rglobig avatar Jan 01 '21 19:01 rglobig

@c0ffeeartc @rglobig thanks for your help. The part above was easy but now I tried to get into the visual part and want to create GameObjects in the scene. I followed this guide

https://github.com/FNGgames/Entitas-Simple-Movement-Unity-Example#addviewsystem

and created this reactive system

    public sealed class AddFieldViewSystem : ReactiveSystem<GameEntity>
    {
        private readonly Transform _fieldViewContainer;
        
        public AddFieldViewSystem(Contexts contexts) : base(contexts.Game)
        {
            GameObject fieldViewContainer = new GameObject("Fields");
            _fieldViewContainer = fieldViewContainer.transform;
        }

        protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
            => context.CreateCollector(GameMatcher.FieldIndex);

        protected override bool Filter(GameEntity entity)
            => entity.HasFieldIndex && !entity.HasView;

        protected override void Execute(List<GameEntity> entities)
        {
            foreach (GameEntity gameEntity in entities)
            {
                Vector2Int fieldIndex = gameEntity.FieldIndex.value;
                GameObject fieldGameObject = new GameObject($"({fieldIndex.x}|{fieldIndex.y})");
                
                fieldGameObject.transform.SetParent(_fieldViewContainer, false);
                
                gameEntity.AddView(fieldGameObject);
                fieldGameObject.Link(gameEntity);
            }
        }
    }

Is this even testable from outside? How would you mock all the things?

obviouslynotthedarklord avatar Jan 02 '21 14:01 obviouslynotthedarklord

How would you mock all the things?

Curious myself, don't know a better way. Maybe not to test some parts...

c0ffeeartc avatar Jan 02 '21 20:01 c0ffeeartc

@c0ffeeartc yes, the same like IRandomService :)

For the SetParent method I think this mock implementation should do it

go.transform.parent = otherTransform

but what else needs to get mocked here? The tests also crash when commenting the whole line out. I think the tests can't deal with GameObjects ... ? Because they live in the scene ... ? (But maybe they can, because it's just an object instance from a type ... )

No Unity related method here but it still crashes

    public sealed class AddFieldViewSystem : ReactiveSystem<GameEntity>
    {
        private readonly Transform _fieldViewContainer;
        
        public AddFieldViewSystem(Contexts contexts) : base(contexts.Game)
        {
            GameObject fieldViewContainer = new GameObject("Fields");
            _fieldViewContainer = fieldViewContainer.transform;
        }

        protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
            => context.CreateCollector(GameMatcher.FieldIndex);

        protected override bool Filter(GameEntity entity)
            => entity.HasFieldIndex && !entity.HasView;

        protected override void Execute(List<GameEntity> entities)
        {
            foreach (GameEntity gameEntity in entities)
            {
                Vector2Int fieldIndex = gameEntity.FieldIndex.value;
                GameObject fieldGameObject = new GameObject($"({fieldIndex.x}|{fieldIndex.y})");

                // ... removed for testing purposes ...
            }
        }
    }

obviouslynotthedarklord avatar Jan 02 '21 20:01 obviouslynotthedarklord

I think the tests can't deal with GameObjects ... ? Because they live in the scene ... ? (But maybe they can, because it's just an object instance from a type ... )

As @rglobig mentioned - Unity related parts don't always work in external tests because they are part of the Untiy C++ Engine.

Removing using UnityEngine from C# script would make IDE to highlight errors of all(or most) Unity related parts. To find a line that crashes test comment code inside constructor and Execute methods, then uncomment them line by line and run test each time until crash.

Some fix options are:

  • Humble Object pattern
  • interacting with View through wrapper
  • writing Unity dependent tests with Unity's NUnit test framework

I personally just wrote tests with NUnit, but then stopped writing them because ServiceLocator pattern in my project made life tougher for testing.

c0ffeeartc avatar Jan 02 '21 21:01 c0ffeeartc

@c0ffeeartc thanks. I thought this would be a basic problem faced by many people and maybe solved by many people :S

but I will give this a try

https://github.com/sschmid/Entitas-CSharp/wiki/How-I-build-games-with-Entitas-(FNGGames)#view-layer-abstraction

obviouslynotthedarklord avatar Jan 02 '21 22:01 obviouslynotthedarklord

I tried to minimize my system

    public sealed class AddFieldViewSystem : ReactiveSystem<GameEntity>
    {
        public AddFieldViewSystem(Contexts contexts) : base(contexts.Game)
        {
        }

        protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
            => context.CreateCollector(GameMatcher.FieldIndex);

        protected override bool Filter(GameEntity entity)
            => entity.HasFieldIndex && !entity.HasView;

        protected override void Execute(List<GameEntity> entities)
        {
            foreach (GameEntity gameEntity in entities)
            {
                GameObject fieldGameObject = new GameObject();
                gameEntity.AddView(fieldGameObject);
                fieldGameObject.Link(gameEntity);
            }
        }
    }

and created a minimized test

    public class Foo
    {
        [Fact]
        private void Bar()
        {
            // Throws 'System.Security.SecurityException: ECall methods must be packaged into a system module.'
            GameObject fieldGameObject = new GameObject();
            
            Assert.True(true);
        }
    }

obviouslynotthedarklord avatar Jan 03 '21 10:01 obviouslynotthedarklord

I would not try to mock View Systems. This is not simulation/game logic code and therefore it's imo not necessary to test it outside of unity. I would write tests inside the view e.g. Unity with NUnit to test logic like this.

rglobig avatar Jan 03 '21 10:01 rglobig

Logic that requires Unity to run can be tested using https://docs.unity3d.com/Manual/testing-editortestsrunner.html

sschmid avatar Sep 07 '22 09:09 sschmid