Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to instantiate MonoBehaviour objects in a unit test on Unity Game Engine platform

I have the following open source project on Github (game project). I'm currently trying to unit test the code I wrote with the MSTest framework but all the tests return the same error message : "Unhandled Exception: System.Security.SecurityException: ECall methods must be packaged into a system module." This happened when I tried to unit test with the NUnit template.

I've looked through the ECall methods post must be packaged to find some answers but I did not because the OP said that his solution work when inside the debugger region but not outside of it. The issue of the OP, as far as I'm concerned, when looking at the post, was not resolved.

Afterwards, I imported the UnityTestTools framework inside my project. Thought it would be easy enough since it was based on the NUnit framework. Turns out that no. The test in itself is fairly basic. I have this base class, called BaseCharacterClass:MonoBehavior, which has, among other things, the property of type BaseCharacterStats. In the stats, there an object of type CharacterHealth which, well, takes care of the health of a player.

Right now, I have the two following stack traces that I don't seem to get when I tried the following in my test.

UNIT TESTS (NUNIT)

  1. Creating MonoBehavior Object using new keyword

    [Test]
    [Category("Mock Character")]
    public void Mock_Character_With_No_Health()
    {
        var mock = new MoqBaseCharacter ();
        Assert.NotNull (mock.BaseStats);
        Assert.NotNull (mock.BaseStats.Health);
        Assert.LessOrEqual (0, mock.BaseStats.Health.CurrentHealth);
    }
    //This is not the full file
    //There "2" classes: 1 for holding tests and that Mock object 
    public MoqBaseCharacter()
    {
        this.BaseStats = new BaseCharacterStats ();
        this.BaseStats.Health = new CharacterHealth (0);
    }
    

Stack Trace :

Mock_Character_With_No_Health (0.047s) --- System.NullReferenceException : Object reference not set to an instance of an object --- at Assets.Scripts.CharactersUtil.CharacterHealth..ctor (Int32 sh) [0x0002f] in C:\Users\Kevin\Documents\AndroidPC_Prototype\PC_Augmented_Tactics_Demo\Assets\Scripts\CharactersUtil\CharacterHealth.cs:29

at UnityTest.MoqBaseCharacter..ctor () [0x00011] in C:\Users\Kevin\Documents\AndroidPC_Prototype\PC_Augmented_Tactics_Demo\Assets\UnityTestTools\Examples\UnitTestExamples\Editor\SampleTests.cs:14

at UnityTest.SampleTests.Mock_Character_With_No_Health () [0x00000] in C:\Users\Kevin\Documents\AndroidPC_Prototype\PC_Augmented_Tactics_Demo\Assets\UnityTestTools\Examples\UnitTestExamples\Editor\SampleTests.cs:32

  1. Using NSubstitute.For

    [Test]
    [Category("Mock Character")]
    public void Mock_Character_With_No_Health()
    {
        var mock = NSubstitute.Substitute.For<MoqBaseCharacter> ();
        Assert.NotNull (mock.BaseStats);
        Assert.NotNull (mock.BaseStats.Health);
        Assert.LessOrEqual (0, mock.BaseStats.Health.CurrentHealth);
    }
    

Stack trace

Mock_Character_With_No_Health (0.137s) --- System.Reflection.TargetInvocationException : Exception has been thrown by the target of an invocation. ----> System.NullReferenceException : Object reference not set to an instance of an object --- at System.Reflection.MonoCMethod.Invoke (System.Object obj, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x0012c] in /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Reflection/MonoMethod.cs:519

at System.Reflection.MonoCMethod.Invoke (BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00000] in /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Reflection/MonoMethod.cs:528

at System.Activator.CreateInstance (System.Type type, BindingFlags bindingAttr, System.Reflection.Binder binder, System.Object[] args, System.Globalization.CultureInfo culture, System.Object[] activationAttributes) [0x001b8] in /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System/Activator.cs:338

at System.Activator.CreateInstance (System.Type type, System.Object[] args, System.Object[] activationAttributes) [0x00000] in /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System/Activator.cs:268

at System.Activator.CreateInstance (System.Type type, System.Object[] args) [0x00000] in /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System/Activator.cs:263

at Castle.DynamicProxy.ProxyGenerator.CreateClassProxyInstance (System.Type proxyType, System.Collections.Generic.List`1 proxyArguments, System.Type classToProxy, System.Object[] constructorArguments) [0x00000] in :0

at Castle.DynamicProxy.ProxyGenerator.CreateClassProxy (System.Type classToProxy, System.Type[] additionalInterfacesToProxy, Castle.DynamicProxy.ProxyGenerationOptions options, System.Object[] constructorArguments, Castle.DynamicProxy.IInterceptor[] interceptors) [0x00000] in :0

at NSubstitute.Proxies.CastleDynamicProxy.CastleDynamicProxyFactory.CreateProxyUsingCastleProxyGenerator (System.Type typeToProxy, System.Type[] additionalInterfaces, System.Object[] constructorArguments, IInterceptor interceptor, Castle.DynamicProxy.ProxyGenerationOptions proxyGenerationOptions) [0x00000] in :0

at NSubstitute.Proxies.CastleDynamicProxy.CastleDynamicProxyFactory.GenerateProxy (ICallRouter callRouter, System.Type typeToProxy, System.Type[] additionalInterfaces, System.Object[] constructorArguments) [0x00000] in :0

at NSubstitute.Proxies.ProxyFactory.GenerateProxy (ICallRouter callRouter, System.Type typeToProxy, System.Type[] additionalInterfaces, System.Object[] constructorArguments) [0x00000] in :0

at NSubstitute.Core.SubstituteFactory.Create (System.Type[] typesToProxy, System.Object[] constructorArguments, SubstituteConfig config) [0x00000] in :0

at NSubstitute.Core.SubstituteFactory.Create (System.Type[] typesToProxy, System.Object[] constructorArguments) [0x00000] in :0

at NSubstitute.Substitute.For (System.Type[] typesToProxy, System.Object[] constructorArguments) [0x00000] in :0

at NSubstitute.Substitute.For[MoqBaseCharacter] (System.Object[] constructorArguments) [0x00000] in :0

at UnityTest.SampleTests.Mock_Character_With_No_Health () [0x00000] in C:\Users\Kevin\Documents\AndroidPC_Prototype\PC_Augmented_Tactics_Demo\Assets\UnityTestTools\Examples\UnitTestExamples\Editor\SampleTests.cs:32 --NullReferenceException

at Assets.Scripts.CharactersUtil.CharacterHealth..ctor (Int32 sh) [0x0002f] in C:\Users\Kevin\Documents\AndroidPC_Prototype\PC_Augmented_Tactics_Demo\Assets\Scripts\CharactersUtil\CharacterHealth.cs:29

at UnityTest.MoqBaseCharacter..ctor () [0x00011] in C:\Users\Kevin\Documents\AndroidPC_Prototype\PC_Augmented_Tactics_Demo\Assets\UnityTestTools\Examples\UnitTestExamples\Editor\SampleTests.cs:14

at Castle.Proxies.MoqBaseCharacterProxy..ctor (ICallRouter , Castle.DynamicProxy.IInterceptor[] ) [0x00000] in :0

at (wrapper managed-to-native) System.Reflection.MonoCMethod:InternalInvoke (object,object[],System.Exception&)

at System.Reflection.MonoCMethod.Invoke (System.Object obj, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00119] in /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Reflection/MonoMethod.cs:513

Disclaimer

A quick reading on NSubstitute showed me that I should better use interfaces for subs... In my situation, I don't really see how an interface would be better for my code. If anyone has an idea for this instead of using the new keyword, I'm all up for it ! Finally, this is the source code for BaseCharacter, BaseStats and Health

Base Character implementation

using System;
using UnityEngine;
using System.Collections.Generic;
using JetBrains.Annotations;
using Random = System.Random;

namespace Assets.Scripts.CharactersUtil
{
    public class BaseCharacterClass : MonoBehaviour
    {
        //int[] basicUDLRMovementArray = new int[4];

        public List<BaseCharacterClass> CurrentEnnemies; 
        public int StartingHealth = 500;
        public BaseCharacterStats BaseStats { get; set; }

        // Use this for initialization
        private void Start()
        {
            BaseStats = new BaseCharacterStats {Health = new CharacterHealth(StartingHealth)}; //Testing purposes
            BaseStats.ChanceForCriticalStrike = new Random().Next(0,BaseStats.CriticalStrikeCounter);
        }

        // Update is called once per frame

        private void Update()
        {
            //ExecuteBasicMovement();

        }

        //During an attack with any kind of character
        //TODO: Make sure that people from the same team cannot attack themselves (friendly fire)
        private void OnTriggerEnter([NotNull] Collider other)
        {
            if (other == null) throw new ArgumentNullException(other.tag);
            Debug.Log("I'm about to receive some damage");
            var characterStats = other.gameObject.GetComponent<BaseCharacterClass>().BaseStats;
            var heathToAddOrRemove = other.gameObject.tag == "Healer" || other.gameObject.tag == "AIHealer" ? characterStats.Power : -1 * characterStats.Power;
            characterStats.Health.TakeDamageFromCharacter((int)heathToAddOrRemove);
            Debug.Log("I should have received damage from a bastard");
            if (characterStats.Health.CurrentHealth == 500)
            {
                Debug.Log("This is a mistake, I believe I'm a god! INVICIBLE");
            }
        }

        /*
        public void ExecuteBasicMovement()
        {
            var move = new Vector3(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), 0);
            transform.position += move * BaseStats.Speed * Time.deltaTime;
        }

        //TODO: Make sure players moves correctly within the environment per cases
        public void ExecuteMovementPerCase()
        {
        }
        */

        public bool CanDoExtraDamage()
        {
            if (BaseStats.ChanceForCriticalStrike*BaseStats.Luck < 50) return false;
            BaseStats.CriticalStrikeCounter--;
            BaseStats.ChanceForCriticalStrike = new Random().Next(0, BaseStats.CriticalStrikeCounter);
            BaseStats.AjustCriticalStrikeChances(); 
            return true;
        }
    }
}

Base Stats

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using JetBrains.Annotations;

namespace Assets.Scripts.CharactersUtil
{
    public class BaseCharacterStats
    {
        public float Power { get; set; }
        public float Defense { get; set; }
        public float Agility { get; set; }
        public float Speed { get; set; } 
        public float MagicPower { get; set; }
        public float MagicResist { get; set; }
        public int ChanceForCriticalStrike;
        public int Luck { get; set; }
        public int CriticalStrikeCounter = 20;
        public int TemporaryDefenseBonusValue;
        private Random _randomValueGenerator;

        public BaseCharacterStats()
        {
            _randomValueGenerator= new Random();
        }

        [NotNull]
        public CharacterHealth Health
        {
            get { return _health; }
            set { _health = value; }
        }
        private CharacterHealth _health;

        public void AjustCriticalStrikeChances()
        {
            if (CriticalStrikeCounter <= 5)
            {
                CriticalStrikeCounter = 5;
            }
        }

        public int DetermineDefenseBonusForTurn()
        {
            TemporaryDefenseBonusValue = _randomValueGenerator.Next(10,20);
            return TemporaryDefenseBonusValue;
        }
    }
}

Health

using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.UI;

namespace Assets.Scripts.CharactersUtil
{
    public class CharacterHealth {
        public int StartingHealth { get; set; }
        public int CurrentHealth { get; set; }
        public Slider HealthSlider { get; set; }
        public bool isDead;
        public Color MaxHealthColor = Color.green;
        public Color MinHealthColor = Color.red;
        private int _counter;
        private const int MaxHealth = 200;
        public Image Fill;


        private void Awake() {
            //HealthSlider = GameObject.GetComponent<Slider>();
            _counter = MaxHealth;            // just for testing purposes
        }
        // Use this for initialization

        public CharacterHealth(int sh)
        {
            StartingHealth = sh;
            CurrentHealth = StartingHealth;
            HealthSlider.wholeNumbers = true; 
            HealthSlider.minValue = 0f;
            HealthSlider.maxValue = StartingHealth;
            HealthSlider.value = CurrentHealth; 
        }

        public void Start()
        {
            HealthSlider.wholeNumbers = true; 
            HealthSlider.minValue = 0f;
            HealthSlider.maxValue = MaxHealth;
            HealthSlider.value = MaxHealth;  
        }

        public void TakeDamageFromCharacter([NotNull] BaseCharacterClass baseCharacter)
        {
            CurrentHealth -= (int)baseCharacter.BaseStats.Power;
            HealthSlider.value = CurrentHealth;
            UpdateHealthBar ();
            if (CurrentHealth <= 0)
                isDead = true;
        }

        public void TakeDamageFromCharacter(int characterStrength)
        {
            CurrentHealth -= characterStrength;
            HealthSlider.value = CurrentHealth;
            UpdateHealthBar ();
            if (CurrentHealth <= 0)
                isDead = true;
        }

        public void RestoreHealth(BaseCharacterClass bs)
        {
            CurrentHealth += (int)bs.BaseStats.Power;
            HealthSlider.value = CurrentHealth;
            UpdateHealthBar ();
        }
        public void RestoreHealth(int characterStrength)
        {
            CurrentHealth += characterStrength;
            HealthSlider.value = CurrentHealth;
            UpdateHealthBar ();
        }
        public void UpdateHealthBar() {
            Fill.color = Color.Lerp(MinHealthColor, MaxHealthColor, (float)CurrentHealth / MaxHealth);
        }
    }
}
like image 445
Kevin Avignon Avatar asked Oct 13 '15 05:10

Kevin Avignon


1 Answers

There's another option to Unit test MonoBehaviours without calling the constructor (using FormatterServices). Here's a small helper class that creates testable MonoBehaviours:

public static class TestableObjectFactory {
    public static T Create<T>() {
        return FormatterServices.GetUninitializedObject(typeof(T)).CastTo<T>();
    }
}

Usage:

var testableObject = TestableObjectFactory.Create<MyMonoBehaviour>();
testableObject.Test();
like image 97
Ilya Suzdalnitski Avatar answered Sep 27 '22 23:09

Ilya Suzdalnitski