5

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);
        }
    }
}
Tiago S
  • 1,299
  • 23
  • 23
Kevin Avignon
  • 2,853
  • 3
  • 19
  • 40
  • 2
    I could be wrong but the problem itself is most likely due to the fact that you're trying to create a new instance of the MonoBehavior object. Which you should never do yourself as Unity needs to do it for you because there are a lot of underlying components that needs to be in the right place for everything to work. So when writing unit tests, you can't test MonoBehaviors themselves, you have to put the "Core Game Logic" in their respectively classes and test those separetely; and only have "Graphical Game Logic" inside the monobehaviors. And then you will be able to run your unit tests :-) – Zerratar Oct 13 '15 at 13:47
  • Another thing, is that you're referencing to the "HealthSlider" in the constructor of the CharacterHealth class which is a unity UI Slider; and you have not set the reference anywhere in the code. So that object is NULL. And will throw a NullPointerException. – Zerratar Oct 13 '15 at 13:52
  • How should I then modified the code inside what I wrote ? – Kevin Avignon Oct 13 '15 at 14:13
  • Basically, should my BaseCharacterClass, class that is inherited by all human players and my AI classes, be a MonoBehaviour child ? @KarlPatrikJohansson – Kevin Avignon Oct 13 '15 at 14:23
  • Your BaseCharacterClass should not be a MonoBehavior, instead, put all the code that requires to be a monobehavior in its own MonoBehavior and reference the BaseCharacterClass from that one. So for instance; When getting the stats of another player (inside your `OnTriggerEnter`), you would do `var characterStats = other.gameObject.GetComponent().BaseCharacterClass.BaseStats;` instead. And keep the logic for the BaseCharacterClass separated from the interaction you get from the MonoBehavior. – Zerratar Oct 13 '15 at 14:35

2 Answers2

8

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();
Ilya Suzdalnitski
  • 52,598
  • 51
  • 134
  • 168
3

Base Character

Class to use for Unit Testing

public class BaseCharacterClass 
{
    public BaseCharacterStats BaseStats { get; set; }
    public BaseCharacterClass(int startingHealth) 
    {
        BaseStats = new BaseCharacterStats {Health = new CharacterHealth(startingHealth)}; //Testing purposes
        BaseStats.ChanceForCriticalStrike = new Random().Next(0,BaseStats.CriticalStrikeCounter);
    }

    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;
    }
}

New MonoBehavior Script to be used for your characters/AI/NPCS

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

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

        public List<BaseCharacterClass> CurrentEnnemies; 
        public int StartingHealth = 500;        

        public BaseCharacterClass CharacterClass;


        public CharacterHealthUI HealthUI;

        // Use this for initialization
        private void Start()
        {
            CharacterClass = new BaseCharacterClass(StartingHealth);  
            HealthUI = this.GetComponent<CharacterHealthUI>();
            HealthUI.CharacterHealth = CharacterClass.BaseStats.Health;
        }

        // 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<BaseCharacterClassWrapper>().CharacterClass.BaseStats;

            var healthToAddOrRemove = other.gameObject.tag == "Healer" || other.gameObject.tag == "AIHealer" ? characterStats.Power : -1 * characterStats.Power;

            characterStats.Health.TakeDamageFromCharacter((int)healthToAddOrRemove);

            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()
        {
            return CharacterClass.CanDoExtraDamage();
        }
    }
}

Health

Use this for your health UI

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

namespace Assets.Scripts.CharactersUtil
{
    public class CharacterHealthUI : MonoBehavior {
      public Image Fill;
      public Color MaxHealthColor = Color.green;
      public Color MinHealthColor = Color.red;   
      public Slider HealthSlider;

      private void Start() {
          if(!HealthSlider) {
            HealthSlider = this.GetComponent<Slider>();            
          }
          if(!Fill) {
            Fill = this.GetComponent<Image>();
          }          
      }

      private CharacterHealth _charaHealth;
      public CharacterHealth CharacterHealth { 
        get { return _charaHealth; }
        set { 
        if(_charaHealth!=null)
            _charaHealth.HealthChanged -= HealthChanged;
          _charaHealth = value; 
          _charaHealth.HealthChanged += HealthChanged;
        }
      }

      public HealthChanged(object sender, HealthChangedEventArgs hp) {
            HealthSlider.wholeNumbers = true; 
            HealthSlider.minValue = hp.MinHealth;
            HealthSlider.maxValue = hp.MaxHealth;
            HealthSlider.value = hp.CurrentHealth;  
            Fill.color = Color.Lerp(MinHealthColor, MaxHealthColor, (float)hp.CurrentHealth / hp.MaxHealth);
      }

    }

}

And Finally, your health logic :-)

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

namespace Assets.Scripts.CharactersUtil
{      

    public class HealthChangedEventArgs : EventArgs 
    {
        public float MinHealth { get; set; }
        public float MaxHealth { get; set; }
        public float CurrentHealth { get; set;}
        public HealthChangedEventArgs(float minHealth, float curHealth, float maxHealth) {
            MinHealth = minHealth;
            CurrentHealth = curHealth;
            MaxHealth = maxHealth;
        }
    }


    public class CharacterHealth {
        public int StartingHealth { get; set; }

        private int _currentHealth;
        public int CurrentHealth 
        { 
          get { return _currentHealth; } 
          set { 
              _currentHealth = value;
              if(HealthChanged!=null)
                HealthChanged(this, new HealthChangedEventArgs(0f, _currentHealth, MaxHealth);
            }
        }      

        public bool isDead;

        private int _counter;
        private const int MaxHealth = 200;

        public event EventHandler<HealthChangedEventArgs> HealthChanged;

        // Use this for initialization

        public CharacterHealth(int sh)
        {
            StartingHealth = sh;
            CurrentHealth = StartingHealth;
        }

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

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

        public void RestoreHealth(BaseCharacterClass bs)
        {
            CurrentHealth += (int)bs.BaseStats.Power;
        }
        public void RestoreHealth(int characterStrength)
        {
            CurrentHealth += characterStrength;
        }
    }
}

This should make it possible for you to unit test the game logic :-)

I have not tested this though, so I cant say for sure that it will work. But logically (in my head at least) it should.

The biggest difference would be that you want to use the BaseCharacterClassWrapper and CharacterHealthUI on your GameObjects to achieve the wanted behavior. And then the Unit Testing goes on BaseCharacterClass and CharacterHealth

I hope this helped!

Zerratar
  • 1,229
  • 11
  • 10
  • So, if I get that right, my unit tests, in this situation, all I have to do is target BaseCharacter and CharacterHealth But when I give the scripts to GameObjects, I give the Wrapper and the UI script ^ – Kevin Avignon Oct 13 '15 at 18:40
  • From the CharacterHealth, you've removed the Image and Slider fields plus how the health bar would have been updated with current health. What should I do ? – Kevin Avignon Oct 13 '15 at 19:22
  • I added a event called HealthChanged that will trigger everytime the health is changed, this will also update the Healthbar (Your slider) for you. So you do not have to think about that part in your health logic. Isolating the different areas making the logic itself testable :-) – Zerratar Oct 14 '15 at 05:55