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

How to automate unit testing of object state?

我有这个可序列化的 class,这是我用来保存游戏数据的 class。

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

利用这个可序列化的 class 的 class 看起来像这样:

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 在同一个文件中 class。

如您所见,GameData class 包含大量内容,为 SaveLoadGameData class 中的每个函数创建测试是一个漫长而乏味的过程。我必须为 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);
}

由于 Helper 是静态的 class 仅包含 public 常量,我不得不使用 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));
}

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