Kevin Avignon Kevin Avignon - 2 months ago 28
C# Question

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

Answer Source

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!