sschmid / Entitas

Entitas is a super fast Entity Component System (ECS) Framework specifically made for C# and Unity
MIT License
7.08k stars 1.11k forks source link

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

Closed obviouslynotthedarklord closed 2 years ago

obviouslynotthedarklord commented 3 years ago

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?

c0ffeeartc commented 3 years ago

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

rglobig commented 3 years ago

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#).

obviouslynotthedarklord commented 3 years ago

@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?

c0ffeeartc commented 3 years ago

How would you mock all the things?

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

obviouslynotthedarklord commented 3 years ago

@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 ...
            }
        }
    }
c0ffeeartc commented 3 years ago

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:

obviouslynotthedarklord commented 3 years ago

@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 commented 3 years ago

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);
        }
    }
rglobig commented 3 years ago

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.

sschmid commented 2 years ago

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