如何自动化对象状态的单元测试?

时间:2016-02-26 10:43:34

标签: 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 = 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代码,测试带有存根和模拟的MonoBehaviors几乎是不可能的。因此我创建了创建假对象的辅助函数:

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

您如何自动完成此过程?

是的,在一次测试中有两个断言是不好的做法,但你会做什么呢?

SaveForWeb()的示例测试

[Test]
public void SaveForWebTest_CreateFakeGameStateObjectRunTheFunctionAndCheckIfLongestChainKeyExists_PassesIfLongestChainKeyExistsInPlayerPrefs()
{
    // arrange
    saveLoadObject = CreateFakeSaveLoadObject();

    // act
    saveLoadObject.SaveForWeb();

    // assert
    Assert.True(PlayerPrefs.HasKey(Helper.LONGEST_CHAIN_KEY));
    Assert.AreEqual(saveLoadObject.longestChain, PlayerPrefs.GetInt(Helper.LONGEST_CHAIN_KEY, Helper.DEFAULT_LONGEST_CHAIN));

    GameObject.DestroyImmediate(gameStateObject);
}

1 个答案:

答案 0 :(得分:0)

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

注意:默认值和helperFields的长度与我检查helperFields在使用ResetGameState()后是否具有默认值的长度相同。 虽然这个答案是关于ResetGameState()而不是SaveForWeb()函数,但是这个代码可以在任何可能的地方应用。