C# 如何自动化对象状态的单元测试?
我有一个可序列化的类,它是用于持久化游戏数据的类C# 如何自动化对象状态的单元测试?,c#,unit-testing,unity3d,nunit,nsubstitute,C#,Unit Testing,Unity3d,Nunit,Nsubstitute,我有一个可序列化的类,它是用于持久化游戏数据的类 [Serializable] class GameData { public float experience = Helper.DEFAULT_EXPERIENCE; public float score = Helper.DEFAULT_SCORE; public float winPercent = Helper.DEFAULT_WIN_PERCENT; public int tasksSolved = He
[Serializable]
class GameData
{
public float experience = Helper.DEFAULT_EXPERIENCE;
public float score = Helper.DEFAULT_SCORE;
public float winPercent = Helper.DEFAULT_WIN_PERCENT;
public int tasksSolved = Helper.DEFAULT_NUM_OF_TASKS_SOLVED;
public int correct = Helper.DEFAULT_NUM_OF_CORRECT;
public int additions = Helper.DEFAULT_NUM_OF_ADDITIONS;
public int subtractions = Helper.DEFAULT_NUM_OF_SUBTRACTIONS;
public bool useAddition = Helper.DEFAULT_USE_ADDITION;
public bool useSubtraction = Helper.DEFAULT_USE_SUBTRACTION;
public bool useIncrementalRange = Helper.DEFAULT_USE_INCREMENTAL_RANGE;
public bool gameStateDirty = Helper.DEFAULT_GAME_STATE_DIRTY;
public bool gameIsNormal = Helper.DEFAULT_GAME_IS_NORMAL;
public bool operandsSign = Helper.DEFAULT_OPERANDS_SIGN;
}
使用此可序列化类的类如下所示:
using UnityEngine;
using System;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
public class SaveLoadGameData : MonoBehaviour
{
public static SaveLoadGameData gameState;
public float experience = Helper.DEFAULT_EXPERIENCE;
public float score = Helper.DEFAULT_SCORE;
public float winPercent = Helper.DEFAULT_WIN_PERCENT;
public int tasksSolved = Helper.DEFAULT_NUM_OF_TASKS_SOLVED;
public int correct = Helper.DEFAULT_NUM_OF_CORRECT;
public int additions = Helper.DEFAULT_NUM_OF_ADDITIONS;
public int subtractions = Helper.DEFAULT_NUM_OF_SUBTRACTIONS;
public bool useAddition = Helper.DEFAULT_USE_ADDITION;
public bool useSubtraction = Helper.DEFAULT_USE_SUBTRACTION;
public bool useIncrementalRange = Helper.DEFAULT_USE_INCREMENTAL_RANGE;
public bool gameStateDirty = Helper.DEFAULT_GAME_STATE_DIRTY;
public bool gameIsNormal = Helper.DEFAULT_GAME_IS_NORMAL;
public bool operandsSign = Helper.DEFAULT_OPERANDS_SIGN;
void Awake () {}
public void init ()
{
if (gameState == null)
{
DontDestroyOnLoad(gameObject);
gameState = this;
}
else if (gameState != this)
{
Destroy(gameObject);
}
}
public void SaveForWeb ()
{
UpdateGameState();
try
{
PlayerPrefs.SetFloat(Helper.EXP_KEY, experience);
PlayerPrefs.SetFloat(Helper.SCORE_KEY, score);
PlayerPrefs.SetFloat(Helper.WIN_PERCENT_KEY, winPercent);
PlayerPrefs.SetInt(Helper.TASKS_SOLVED_KEY, tasksSolved);
PlayerPrefs.SetInt(Helper.CORRECT_ANSWERS_KEY, correct);
PlayerPrefs.SetInt(Helper.ADDITIONS_KEY, additions);
PlayerPrefs.SetInt(Helper.SUBTRACTIONS_KEY, subtractions);
PlayerPrefs.SetInt(Helper.USE_ADDITION, Helper.BoolToInt(useAddition));
PlayerPrefs.SetInt(Helper.USE_SUBTRACTION, Helper.BoolToInt(useSubtraction));
PlayerPrefs.SetInt(Helper.USE_INCREMENTAL_RANGE, Helper.BoolToInt(useIncrementalRange));
PlayerPrefs.SetInt(Helper.GAME_STATE_DIRTY, Helper.BoolToInt(gameStateDirty));
PlayerPrefs.SetInt(Helper.OPERANDS_SIGN, Helper.BoolToInt(operandsSign));
PlayerPrefs.Save();
}
catch (Exception ex)
{
Debug.Log(ex.Message);
}
}
public void SaveForX86 () {}
public void Load () {}
public void UpdateGameState () {}
public void ResetGameState () {}
}
注意:GameData与SaveLoadGameData类位于同一文件中
正如您所看到的,GameData类有很多东西,在SaveLoadGameData类中为每个函数创建测试是一个漫长而乏味的过程。我必须为GameData中的每个属性创建一个假对象,并测试SaveLoadGameData中函数的功能—它们是否执行了它们应该执行的操作
注意:这是Unity3D 5代码,使用存根和模拟测试单行为几乎是不可能的。因此,我创建了创建伪对象的辅助函数:
SaveLoadGameData saveLoadObject;
GameObject gameStateObject;
SaveLoadGameData CreateFakeSaveLoadObject ()
{
gameStateObject = new GameObject();
saveLoadObject = gameStateObject.AddComponent<SaveLoadGameData>();
saveLoadObject.init();
saveLoadObject.experience = Arg.Is<float>(x => x > 0);
saveLoadObject.score = Arg.Is<float>(x => x > 0);
saveLoadObject.winPercent = 75;
saveLoadObject.tasksSolved = 40;
saveLoadObject.correct = 30;
saveLoadObject.additions = 10;
saveLoadObject.subtractions = 10;
saveLoadObject.useAddition = false;
saveLoadObject.useSubtraction = false;
saveLoadObject.useIncrementalRange = true;
saveLoadObject.gameStateDirty = true;
saveLoadObject.gameIsNormal = false;
saveLoadObject.operandsSign = true;
return saveLoadObject;
}
由于Helper是只包含公共常量的静态类,我必须使用BindingFlags.static和BindingFlags.public来迭代其成员,因此我使用此代码段来自动断言不同类型的几个字段:
FieldInfo[] helperFields = typeof(SaveLoadGameData).GetFields();
FieldInfo[] defaults = typeof(Helper).GetFields(BindingFlags.Static | BindingFlags.Public);
for(int i = 0; i < defaults.Length; i += 1)
{
Debug.Log(helperFields[i].Name + ", " + helperFields[i].GetValue(saveLoadObject) + ", " + defaults[i].GetValue(null));
Assert.AreEqual(helperFields[i].GetValue(saveLoadObject), defaults[i].GetValue(null));
}
FieldInfo[]helperFields=typeof(SaveLoadGameData).GetFields();
FieldInfo[]默认值=typeof(Helper).GetFields(BindingFlags.Static | BindingFlags.Public);
对于(int i=0;i
注意:使用ResetGameState()后,默认值和helperFields的长度与我检查helperFields是否具有默认值的长度相同。
虽然这个答案是关于ResetGameState()而不是SaveForWeb()函数,但只要有可能,就可以应用这段代码。因为Helper是只包含公共常量的静态类,所以我必须使用BindingFlags.static和BindingFlags.public来迭代其成员,因此,我使用此代码片段对不同类型的几个字段进行自动断言:
FieldInfo[] helperFields = typeof(SaveLoadGameData).GetFields();
FieldInfo[] defaults = typeof(Helper).GetFields(BindingFlags.Static | BindingFlags.Public);
for(int i = 0; i < defaults.Length; i += 1)
{
Debug.Log(helperFields[i].Name + ", " + helperFields[i].GetValue(saveLoadObject) + ", " + defaults[i].GetValue(null));
Assert.AreEqual(helperFields[i].GetValue(saveLoadObject), defaults[i].GetValue(null));
}
FieldInfo[]helperFields=typeof(SaveLoadGameData).GetFields();
FieldInfo[]默认值=typeof(Helper).GetFields(BindingFlags.Static | BindingFlags.Public);
对于(int i=0;i
注意:使用ResetGameState()后,默认值和helperFields的长度与我检查helperFields是否具有默认值的长度相同。
虽然这个答案是关于ResetGameState()而不是SaveForWeb()函数,但只要可能,这段代码都可以应用。我很困惑。
GameData
与SaveLoadGameData
的关系如何?行saveLoadObject.experience
如何编译SaveLoadGameData
没有名为experience
的属性,或者我遗漏了什么?在您的[Test]
中,为助手设置一个先验值有什么意义。最长链键
到默认最长链
(预期值)如果在下一行中,您要求Unity查找,如果不存在,则返回DEFAULT\u LONGEST\u CHAIN
。这是糟糕的测试设计,你不知道实际的结果是你的代码还是统一的结果。如果要将这两个断言分为两个测试,我需要对每个属性进行额外的24个测试,以测试PlayerPrefs中的属性是否等于内存中的属性(SaveLoadObject)。第一个断言检查PlayerPref是否包含该属性,下一个断言检查是否相等。但是你对这两个断言的看法是对的,它们应该是一个单独的测试。对单个属性进行单元测试是没有意义的。单元测试用于测试行为或编排,而不是原子状态。现在您可以为“saveforweb”场景编写一个单元测试,它不仅包括save机制,还包括state。这更有意义。当人们觉得他们必须为每一个公共方法编写一个测试时,情况也是如此——你不需要。您只需要努力获得高代码覆盖率就可以了。你也可以使用反射。但是通常你希望你的测试尽可能简单,否则你需要一个测试来测试:)我很困惑。GameData
与SaveLoadGameData
的关系如何?行saveLoadObject.experience
如何编译SaveLoadGameData
没有名为experience
的属性,或者我遗漏了什么?在您的[Test]
中,为助手设置一个先验值有什么意义。最长链键
到默认最长链
(预期值)如果在下一行中,您要求Unity查找,如果不存在,则返回DEFAULT\u LONGEST\u CHAIN
。这是糟糕的测试设计,你不知道实际的结果是你的代码还是统一的结果。如果要将这两个断言分为两个测试,我需要对每个属性进行额外的24个测试,以测试PlayerPrefs中的属性是否等于内存中的属性(SaveLoadObject)。第一个断言检查PlayerPref是否包含该属性,下一个断言检查是否相等。但是你对这两个断言的看法是对的,它们应该是一个单独的测试。对单个属性进行单元测试是没有意义的。单元测试用于测试行为或编排,而不是原子状态。现在您可以为“saveforweb”场景编写一个单元测试,它不仅包括save机制,还包括state。这更有意义。当人们觉得他们必须为每一个公共方法编写一个测试时,情况也是如此——你不需要。您只需要努力获得高代码覆盖率就可以了。你也可以使用反射。但通常您希望测试尽可能简单,否则您需要测试来测试测试:)