1

I have this serializable class which is my class for persisting game data.

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

The class that utilizes this serializable class looks like this:

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

Note: GameData is inside the same file with SaveLoadGameData class.

As you can see GameData class has ton of stuff and creating test for each function inside SaveLoadGameData class is long and boring process. I have to create a fake object for each property inside GameData and test the functionality of the functions in SaveLoadGameData do they do what they are supposed to do.

Note: This is Unity3D 5 code and testing MonoBehaviors with stubs and mocks is almost immposible. Therefore I created helper function that creates fake object:

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

How would you automate this process?

Yes two asserts inside one test is a bad practice but what would you do instead?

Example test for 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);
}
Vlad
  • 2,739
  • 7
  • 49
  • 100
  • I'm confused. How is `GameData` related to `SaveLoadGameData`? How does the line `saveLoadObject.experience` compile? `SaveLoadGameData` has no property named `experience` or am I missing something? –  Feb 26 '16 at 13:51
  • In your `[Test]`, what's the point of setting a prior value for `Helper.LONGEST_CHAIN_KEY` to `DEFAULT_LONGEST_CHAIN` (the **expected**) if in the next line you ask Unity to look it up and if not present return `DEFAULT_LONGEST_CHAIN` regardless. That's bad test design, you don't know if the **actual** is the result of your code or Unity. –  Feb 26 '16 at 13:58
  • Well yes as I said previosly, two asserts in one test is bad design. If I was to separate those two asserts into two tests I would need additional 24 tests or so for each property just to test if the property in PlayerPrefs is equal to the one in memory(SaveLoadObject). The first assert checks if PlayerPrefs contain the property, the next assert check equality. But yes you are right about the two asserts, they should be a separate tests. – Vlad Feb 26 '16 at 14:37
  • 1
    Unit testing a single _property_ is not meaningful. Unit testing is for testing _behaviour_ or _orchestration_ not _atomic state_. Now you could write a **single** unit test for the _"save for web"_ scenario which would include not only save mechanics but also state. That's more meaningful. The same goes when people feel they must write a test for every single public method - you don't. You just need to strive for a high _code coverage_ that's all –  Feb 26 '16 at 14:40
  • So I could possibly create a Tuple/MultyKeyDictionary which will contain all my game state properties and check them all at once in one test? – Vlad Feb 26 '16 at 14:44
  • 1
    Sure. You could use reflection too. But generally you want your test to be as simple as possible, otherwise you need a test to test the test :) –  Feb 26 '16 at 14:51
  • hi @vlad - I think you just want to automatically save each variable using the "name" as a key - see answer – Fattie Feb 26 '16 at 14:58
  • I already have code for saving/loading data for web and for desktop. And it works. Just wanted to make sure my functions still do what are supposed to do after I add new features to my game. And I am sorry for the code its just too big to post but to explain it it would be even harder. – Vlad Feb 26 '16 at 15:11

1 Answers1

0

Since Helper is static class containing only public constants I had to use BindingFlags.Static and BindingFlags.Public to iterate over its members, so I used this code snippet to automate asserting over several fields of different type:

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

Note: defaults and helperFields have the same length as I am checking if helperFields have the default values after using ResetGameState(). Though this answer is about ResetGameState() instead of SaveForWeb() function, this code can be applied wherever possible.

Vlad
  • 2,739
  • 7
  • 49
  • 100