我在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)
使用新关键字
创建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);
}
堆栈跟踪:
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
使用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);
}
堆栈跟踪
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);
}
}
}
答案 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上使用BaseCharacterClassWrapper
和CharacterHealthUI
来实现想要的行为。然后单元测试继续BaseCharacterClass
和CharacterHealth
我希望这有帮助!