如何在Unity Game Engine平台

时间:2015-10-13 05:17:13

标签: c# .net unit-testing unity3d

我在Github上有以下开源项目(game project)。我目前正在尝试对使用MSTest框架编写的代码进行单元测试,但所有测试都返回相同的错误消息:"未处理的异常:System.Security.SecurityException:必须将ECall方法打包到系统模块中"当我尝试使用NUnit模板进行单元测试时,就会发生这种情况。

我查看了 ECall methods post must be packaged 找到一些答案,但我没有,因为OP说他的解决方案在调试器区域内但不在其外部时工作。就我所关注的那样,OP在查看帖子时的问题没有得到解决。

之后,我在项目中导入了UnityTestTools框架。认为它很容易,因为它基于NUnit框架。事实证明没有。测试本身是相当基础的。我有这个基类,名为BaseCharacterClass:MonoBehavior,其中包含BaseCharacterStats类型的属性。在统计数据中,有一个CharacterHealth类型的对象,它可以处理玩家的健康状况。

现在,当我在测试中尝试以下内容时,我似乎无法获得以下两个堆栈跟踪。

UNIT TESTS(NUNIT)

  1. 使用新关键字

    创建MonoBehavior对象
    [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);
    }
    
  2. 堆栈跟踪

      

    Mock_Character_With_No_Health(0.047s)   --- System.NullReferenceException:对象引用未设置为对象的实例   ---在Assets.Scripts.CharactersUtil.CharacterHealth..ctor(Int32 sh)[0x0002f] in   C:\用户\凯文\文件\ AndroidPC_Prototype \ PC_Augmented_Tactics_Demo \资产\脚本\ CharactersUtil \ CharacterHealth.cs:29

         

    在UnityTest.MoqBaseCharacter..ctor()[0x00011]中   C:\用户\凯文\文件\ AndroidPC_Prototype \ PC_Augmented_Tactics_Demo \资产\ UnityTestTools \实例\ UnitTestExamples \编辑\ SampleTests.cs:14

         

    在UnityTest.SampleTests.Mock_Character_With_No_Health()[0x00000]中   C:\用户\凯文\文件\ AndroidPC_Prototype \ PC_Augmented_Tactics_Demo \资产\ UnityTestTools \实例\ UnitTestExamples \编辑\ SampleTests.cs:32

    1. 使用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);
      }
      
    2. 堆栈跟踪

        

      Mock_Character_With_No_Health(0.137s)   --- System.Reflection.TargetInvocationException:调用目标抛出了异常。 ----&GT;   System.NullReferenceException:对象引用未设置为   对象的实例   ---在System.Reflection.MonoCMethod.Invoke(System.Object obj,BindingFlags invokeAttr,System.Reflection.Binder binder,   System.Object []参数,System.Globalization.CultureInfo文化)   [0x0012c] in   /Users/builduser/buildslave/mono-runtime-and-classlibs/build/mcs/class/corlib/System.Reflection/MonoMethod.cs:519

           

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

           

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

           

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

           

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

           

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

           

      在Castle.DynamicProxy.ProxyGenerator.CreateClassProxy(System.Type   classToProxy,System.Type [] additionalInterfacesToProxy,   Castle.DynamicProxy.ProxyGenerationOptions选项,System.Object []   constructorArguments,Castle.DynamicProxy.IInterceptor []拦截器)   [0x00000] in:0

           

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

           

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

           

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

           

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

           

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

           

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

           

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

           

      在UnityTest.SampleTests.Mock_Character_With_No_Health()[0x00000]中   C:\用户\凯文\文件\ AndroidPC_Prototype \ PC_Augmented_Tactics_Demo \资产\ UnityTestTools \实例\ UnitTestExamples \编辑\ SampleTests.cs:32   --NullReferenceException

           

      在Assets.Scripts.CharactersUtil.CharacterHealth..ctor(Int32 sh)   [0x0002f] in   C:\用户\凯文\文件\ AndroidPC_Prototype \ PC_Augmented_Tactics_Demo \资产\脚本\ CharactersUtil \ CharacterHealth.cs:29

           

      在UnityTest.MoqBaseCharacter..ctor()[0x00011]中   C:\用户\凯文\文件\ AndroidPC_Prototype \ PC_Augmented_Tactics_Demo \资产\ UnityTestTools \实例\ UnitTestExamples \编辑\ SampleTests.cs:14

           

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

           

      at(包装器托管到原生)   System.Reflection.MonoCMethod:InternalInvoke   (对象,对象[],System.Exception的&安培;)

           

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

      声明

      快速阅读NSubstitute告诉我,我应该更好地使用接口...在我的情况下,我真的不知道接口如何更好地代码。如果有人对此有所了解而不是使用新关键字,我就全力以赴!最后,这是BaseCharacter,BaseStats和Health

      的源代码

      基本角色实施

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

      基本统计

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

      健康

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

2 个答案:

答案 0 :(得分:4)

在不调用构造函数(使用FormatterServices)的情况下,单元测试MonoBehaviours的另一个选项。这是一个小助手类,可以创建可测试的MonoBehaviours:

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

用法:

var testableObject = TestableObjectFactory.Create<MyMonoBehaviour>();
testableObject.Test();

答案 1 :(得分:3)

基本字符

用于单元测试的类

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

用于角色/ AI / NPCS的新MonoBehavior脚本

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

<强>健康

将此用于您的运行状况用户界面

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

    }

}

最后,你的健康逻辑:-)

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

这可以让你对游戏逻辑进行单元测试: - )

我没有测试过这个,所以我无法确定它会起作用。但从逻辑上讲(至少在我脑海中)它应该。

最大的区别在于您希望在GameObjects上使用BaseCharacterClassWrapperCharacterHealthUI来实现想要的行为。然后单元测试继续BaseCharacterClassCharacterHealth

我希望这有帮助!