如何测试只有开关逻辑的功能?

How to test a function that has only switch logic?

我有一个 Save() 函数的代码,它里面只有 switch 语句。所以基本上它会根据所选平台进行保存。但是,我已经对 UpdateGameState()、SaveForWeb() 和 SaveForX86() 函数进行了测试。由于单元测试规则说,如果您的函数中有逻辑,无论它多么简单,您都必须测试该函数。

public void Save ()
{
    switch(Helper.BUILD_TYPE)
    {
        case Helper.BUILD_FOR_WEB:
            SaveForWeb();
            break;

        case Helper.BUILD_FOR_WIN_X86:
            SaveForX86();
            break;

        default:
            Debug.Log("Save method: " + Helper.WRONG_BUILD_TYPE_SELECTED_ERR);
            break;
    }
}

在测试中调用测试也打破了测试的隔离规则,所以我似乎必须将测试逻辑复制到我的其他测试中,只是为了检查 Save() 逻辑是否在 SaveForWeb() 和SaveForX86().

在这种情况下,您将如何测试此功能?

我可以在测试中做到这一点:

Helper.BUILD_TYPE = Helper.BUILD_FOR_WEB;

其中 BUILD_TYPE 是静态的,但不像 BUILD_FOR_WEB 和 BUILD_FOR_WIN_X86 那样恒定。

这是正在测试的 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;

    void Awake ()
    {
        Init();
    }

    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.Save();
        }
        catch (Exception ex)
        {
            Debug.Log(ex.Message);
        }
    }

    public void SaveForX86 ()
    {
        UpdateGameState();
        try
        {
            BinaryFormatter bf = new BinaryFormatter();
            FileStream fs = File.Create(Application.persistentDataPath + Helper.GAME_DATA_FILE_NAME);

            GameData data = new GameData();
            data.experience = experience;
            data.score = score;

            bf.Serialize(fs, data);
            fs.Close();
        }
        catch (Exception ex)
        {
            Debug.Log(ex.Message);
        }
    }

    public void Save ()
    {
        switch(Helper.BUILD_TYPE)
        {
            case Helper.BUILD_FOR_WEB:
                SaveForWeb();
                break;

            case Helper.BUILD_FOR_WIN_X86:
                SaveForX86();
                break;

            case Helper.BUILD_FOR_ANDROID:
                break;

            default:
                Debug.Log("Save method: " + Helper.WRONG_BUILD_TYPE_SELECTED_ERR);
                break;
        }
    }

    public void LoadForWeb ()
    {
        try
        {
            experience = PlayerPrefs.GetFloat(Helper.EXP_KEY, Helper.DEFAULT_EXPERIENCE);
            score = PlayerPrefs.GetFloat(Helper.SCORE_KEY, Helper.DEFAULT_SCORE);
        }
        catch (Exception ex)
        {
            Debug.Log(ex.Message);
        }
    }

    public void LoadForX86 ()
    {
        try
        {
            if (File.Exists(Application.persistentDataPath + Helper.GAME_DATA_FILE_NAME))
            {
                BinaryFormatter bf = new BinaryFormatter();
                FileStream fs = File.Open(Application.persistentDataPath + Helper.GAME_DATA_FILE_NAME, FileMode.Open);
                GameData data = (GameData)bf.Deserialize(fs);

                experience = data.experience;
                score = data.score;

                fs.Close();
            }
            else
            {
                Save();
            }
        }
        catch (Exception ex)
        {
            Debug.Log(ex.Message);
        }
    }

    public void Load ()
    {
        switch(Helper.BUILD_TYPE)
        {
            case Helper.BUILD_FOR_WEB:
                LoadForWeb();
                break;

            case Helper.BUILD_FOR_WIN_X86:
                LoadForX86();
                break;

            case Helper.BUILD_FOR_ANDROID:
                break;

            default:
                Debug.Log("Load method: " + Helper.WRONG_BUILD_TYPE_SELECTED_ERR);
                break;
        }
        UpdateGameState();
    }

    public void UpdateGameState ()
    {
        gameState.experience = experience;
        gameState.score = score;
    }

    public void ResetGameState ()
    {
        experience = Helper.DEFAULT_EXPERIENCE;
        score = Helper.DEFAULT_SCORE;

        Save();
    }
}

[Serializable]
class GameData
{
    public float experience = Helper.DEFAULT_EXPERIENCE;
    public float score = Helper.DEFAULT_SCORE;
}

注意:我从 Save() 中删除了 UpdateGameState(),现在它是 SaveForWeb() 和 SaveForX86()。

如果您已经对 UpdateGameState()、SaveForWeb() 等进行了测试,则不应 "double" 测试您的逻辑。您应该只验证在设置不同枚举时调用的方法。这意味着 Save 方法本身可能是它自己的 class,并且依赖于其他方法的一个或多个接口。

根据 testing MonoBehaviors 的本教程,我使用单独的 class 和接口

对 MonoBehavior 功能和其他可测试功能进行了解耦
using System;
using UnityEngine;

namespace Assets.Scripts
{
    /// <summary>
    /// Description of ISaveLoadGameData.
    /// </summary>
    public interface ISaveLoadGameData
    {
        void SaveForWeb();
        void SaveForX86();
        void Save();
        void UpdateGameState();
    }
}

using System;
using UnityEngine;

namespace Assets.Scripts
{
    /// <summary>
    /// Description of SaveLoadGameDataController.
    /// </summary>
    [Serializable]
    public class SaveLoadGameDataController : ISaveLoadGameData
    {
        ISaveLoadGameData slgdInterface;
        GameObject gameObject;

        public static SaveLoadGameDataController gameState;

        public float experience = Helper.DEFAULT_EXPERIENCE;
        public float score = Helper.DEFAULT_SCORE;

        public void SetSaveLoadGameData (ISaveLoadGameData slgd)
        {
            slgdInterface = slgd;
        }

        public void SaveForWeb ()
        {
            slgdInterface.SaveForWeb();
        }

        public void SaveForX86 ()
        {
            slgdInterface.SaveForX86();
        }

        public void Save ()
        {
            slgdInterface.Save();
        }

        public void UpdateGameState ()
        {
            slgdInterface.UpdateGameState();
        }
    }
}

这样我就可以像这样对 Save() 函数进行干净简单的测试:

[Test]
[Category(Helper.TEST_CATEGORY_SAVE_GAME_STATE)]
public void SaveTest_SetBuildTypeToWebAndRunSave_PassesIfSaveFunctionCalledSaveForWebFunction ()
{
    // arrange
    Helper.BUILD_TYPE = Helper.BUILD_FOR_WEB;
    var slgdController = FakeSaveLoadGameDataController();

    // act
    slgdController.ClearReceivedCalls();
    slgdController.Save();

    // assert
    slgdController.Received().SaveForWeb();
}

[Test]
[Category(Helper.TEST_CATEGORY_SAVE_GAME_STATE)]
public void SaveTest_SetBuildTypeToX86AndRunSave_PassesIfSaveFunctionCalledSaveForX86Function ()
{
    // arrange
    Helper.BUILD_TYPE = Helper.BUILD_FOR_WIN_X86;
    var slgdController = FakeSaveLoadGameDataController();

    // act
    slgdController.ClearReceivedCalls();
    slgdController.Save();

    // assert
    slgdController.Received().SaveForX86();

    Helper.BUILD_TYPE = Helper.BUILD_FOR_WEB;
}

FakeSaveLoadGameDataController() 看起来像这样:

SaveLoadGameDataController FakeSaveLoadGameDataController ()
{
    SaveLoadGameDataController slgdController = Substitute.For<SaveLoadGameDataController>();
    ISaveLoadGameData slgd = Substitute.For<ISaveLoadGameData>();
    slgdController.SetSaveLoadGameData(slgd);

    slgdController.experience = Arg.Is<float>(x => x > 0);
    slgdController.score = Arg.Is<float>(x => x > 0);

    return slgdController;
}