0

I have a code of my Save() function, it has only switch statement inside. So basically it saves based on the platform chosen. However I already have tests for UpdateGameState(), SaveForWeb() and SaveForX86() functions. Since Unit Testing rules say, if you have a logic in your function no matter how simple it is, you have to test that function.

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

Also calling test inside a test breaks the isolation rule of tests, so it seems I have to copy the testing logic inside my other tests just to check whether the Save() logic works all the way after SaveForWeb() and SaveForX86().

In these circumstances, how would you test this function?

I can do this in my tests:

Helper.BUILD_TYPE = Helper.BUILD_FOR_WEB;

Where BUILD_TYPE is static but not constant as BUILD_FOR_WEB and BUILD_FOR_WIN_X86.

Here is the class under test:

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

Note: I removed UpdateGameState() from Save() and now it is both SaveForWeb() and SaveForX86().

Vlad
  • 2,739
  • 7
  • 49
  • 100
  • 1
    Do SaveForWeb & SaveForX86 need to be public, or could they be made private and tested through the Save method? Does UpdateGameState need to be called from within Save, it seems like Saving and Updating are two quite different things... You *could* push the logic out into another class, then mock it's interactions. You *could* extract some of the logic from your other tests into common methods that are called by both tests. A lot of it depends on the benefits of the different approaches and how much code (test / production) you're actually talking about... – forsvarir Mar 01 '16 at 19:33

2 Answers2

1

You should not "double" test your logic if you already have tests for UpdateGameState(), SaveForWeb() etc. You should only validate that the methods are called when the different enums are set. This means that the Save-method itself probably could be a class of it's own, and take dependencies on one or more interfaces for the other methods.

andreasnico
  • 1,478
  • 14
  • 23
  • Yes, good thinking. I got another plausible idea, maybe add helper function that holds only asserts(without [Test] anotation) for each of the functions and reuse that helper function in both tests for SaveForWeb(), UpdateGameState(), SaveForX86() and also Save() – Vlad Mar 02 '16 at 08:48
  • 1
    hi @vlad, could you PLEASE "tick" an answer to close out the question and help keep the QA tidy, thanks! – Fattie Mar 02 '16 at 13:41
-1

According to this tutorial for testing MonoBehaviors I made decoupling of the MonoBehavior functionality and the other testable functionality using separate class and an Interface

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

This way I was able to make clean and simple tests for Save() function like this:

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

Where FakeSaveLoadGameDataController() looks like this:

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;
}
Vlad
  • 2,739
  • 7
  • 49
  • 100
  • Maybe creating separated classes for Save and Load would be better but for now this did the job. – Vlad Mar 04 '16 at 09:32