- Home /
How to make MonoBehavior class testable with NSubstitute?
Ton of games have been made with Unity, Hearthstone, Ori and the Blind Forest etc... and they all seem bullet-proof i.e as if they are 100% bug free. So Blizzard must have used some sort of testing for Hearthstone. I am still struggling to understand how to make a class that inherits MonoBehavior testable. You must create GameObject that will hold a MonoBehavior component.
I have this 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 UpdateGameState ()
{
gameState.experience = experience;
gameState.score = score;
}
}
[Serializable]
class GameData
{
public float experience = Helper.DEFAULT_EXPERIENCE;
public float score = Helper.DEFAULT_SCORE;
}
I want to check if UpdateGameState() was called when calling SaveForWeb(). There is a tutorial how to do that with NSubstitute here: http://nsubstitute.github.io/help/received-calls/ But it uses Interface and a class constructor that recieves interface.
how would you make my class testable with NSubstitute? Should SaveLoadGameData implement ISaveLoadGameData interface or I create constructor that calls a function inside?
Let me know your ideas.
Answer by spinnerbox · Mar 03, 2016 at 01:12 PM
According to the 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 Init();
void SaveForWeb();
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 Init ()
{
slgdInterface.Init();
}
public void SaveForWeb ()
{
slgdInterface.SaveForWeb();
}
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_FOR_WEB)]
public void SaveForWebTest_CreateFakeGameStateObjectRunTheFuncAndCheckIfUpdateGameStateIsCalled_PassesIfUpdateGameStateFuncWasCalled ()
{
// arrange
var slgdController = FakeSaveLoadGameDataController();
// act
slgdController.ClearReceivedCalls();
slgdController.SaveForWeb();
// assert
slgdController.Received().UpdateGameState();
}
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;
}
Answer by phil_me_up · Mar 02, 2016 at 01:53 PM
Have you seen the blog post on the Unity Test Tools: http://blogs.unity3d.com/2014/07/28/unit-testing-at-the-speed-of-light-with-unity-test-tools/
In there they give some examples of how you might achieve this, specifically in the Mock object section.
EDIT: Meant to link to this too, which specifically deals with the problems of testing Monobehaviours: http://blogs.unity3d.com/2014/06/03/unit-testing-part-2-unit-testing-monobehaviours/
Yes the tutorials leaded me to the solution but I cannot accept this as an answer. I will post my own how I solved this.
Your answer
Follow this Question
Related Questions
NUnit difference between Assert's IsTrue/IsFalse and True/False 1 Answer
Wait times when developing 3 Answers
Can Unity 5.6's Test Runner optionally output NUnit 2 xml format? 2 Answers
How can I run Unity tests with some defined nUnit category 2 Answers
How to implement mandatory cleanup for the Unity PlayMode tests? 1 Answer