【问题标题】:How to test a function that has only switch logic?如何测试只有开关逻辑的功能?
【发布时间】:2016-06-14 03:56:26
【问题描述】:

我有一个 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;
    }
}

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

在这种情况下,你会如何测试这个功能?

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

Helper.BUILD_TYPE = Helper.BUILD_FOR_WEB;

其中 BUILD_TYPE 是静态的,但不是常量,如 BUILD_FOR_WEB 和 BUILD_FOR_WIN_X86。

这是被测试的类:

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()。

【问题讨论】:

  • SaveForWeb 和 SaveForX86 是否需要公开,或者可以将它们设为私有并通过 Save 方法进行测试? UpdateGameState 是否需要从 Save 中调用,看起来 Saving 和 Updating 是两件完全不同的事情……您可以将逻辑推到另一个类中,然后模拟它的交互。您可以将其他测试中的一些逻辑提取到两个测试都调用的通用方法中。这在很大程度上取决于不同方法的好处以及您实际上在谈论多少代码(测试/生产)......

标签: c# unit-testing unity3d nunit nsubstitute


【解决方案1】:

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

【讨论】:

  • 是的,好主意。我得到了另一个合理的想法,可能会添加仅包含每个函数的断言(没有 [Test] 注释)的辅助函数,并在 SaveForWeb()、UpdateGameState()、SaveForX86() 和 Save( )
  • 嗨@vlad,请您“勾选”一个答案以结束问题并帮助保持质量检查整洁,谢谢!
【解决方案2】:

根据testing MonoBehaviors 的本教程,我使用单独的类和接口对 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;
}

【讨论】:

  • 也许为保存和加载创建单独的类会更好,但现在这已经完成了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-22
  • 2020-02-02
  • 1970-01-01
  • 1970-01-01
  • 2017-12-09
相关资源
最近更新 更多