I would like to learn the basics of testing, how to make a test
I am using the new unity input system (OnMove), I store that input in a vector2, later I use that vector2 in a function that moves the character (ProcessMovementOfShip). The game works, I can move the player around with WASD, but I would love to have a test that verifies that the function responsible for movement works.
I have tried watching a couple of youtube videos about testing, it feels like the entry into tests are getting to steep, I would love to learn it, I can see the importance of it, I just dont know what I am doing and how to solve the problem at hand and I am starting to feel I should just put the whole thing on a shelf and hopefully return to it later.
How do I test that the player has moved?
PlayMode Test
public class player_movement
{
[UnityTest]
public IEnumerator player_moves_when_processship_is_fed_a_vector()
{
var gameObject = new GameObject();
var playerMovement = gameObject.AddComponent<PlayerMovement>();
Vector2 startPosition = playerMovement.transform.position;
playerMovement.ProcessMovementOfShip(new Vector2(1, 0));
yield return new WaitForFixedUpdate();
Vector2 endPosition = playerMovement.transform.position;
Assert.AreNotEqual(startPosition, endPosition);
}
}
EditMode Test
public class Movement
{
[Test]
public void start_position_of_player_is_0()
{
var gameObject = new GameObject();
var playerMovement = gameObject.AddComponent<PlayerMovement>();
var startPostion = playerMovement.transform.position;
playerMovement.ProcessMovementOfShip(new Vector2(1,0));
var endPosition = playerMovement.transform.position.x;
Assert.AreNotEqual(startPostion, endPosition);
}
}
PlayerMovement.cs
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerMovement : MonoBehaviour
{
[Header("Player Movement")]
[Range(5f, 20f)][SerializeField] float _moveSpeed = 15f;
private Rigidbody2D _rigidBody;
private Vector2 _rawInput;
void Awake()
{
_rigidBody = GetComponent<Rigidbody2D>();
if (_rigidBody == null) Debug.Log("No RigidBody2D detected!");
}
void FixedUpdate()
{
ProcessMovementOfShip(_rawInput);
}
public void ProcessMovementOfShip(Vector2 input)
{
Vector3 delta = input * _moveSpeed * Time.fixedDeltaTime;
delta = transform.position;
_rigidBody.MovePosition(delta);
}
private void OnMove(InputValue value)
{
Vector2 _rawInput = value.Get<Vector2>();
}
}
error I try to check that the position of the character has changed, I get a "NullReferenceException" System.NullReferenceException : Object reference not set to an instance of an object
CodePudding user response:
You would need to decouple the class with what Unity does and what you do. I'll go with a simple example to demonstrate.
public class Provider : MonoBehaviour, IProvider
{
[SerializeField] private SomeClass m_someClass;
private Logic m_logic;
void Start()
{
m_logic = new Logic(this);
}
}
public interface IProvider
{}
public class Logic
{
private IProvider m_provider;
public Logic (IProvider provider)
{
m_provider = provider;
}
public int MethodToTest(int someValue)
{
}
}
At this point, we have three parts, the unity part where you will put everything that is Unity related, called Provider. This can be the Unity lifecycle such as Update, Start, any event related that the engine reports and all the connections. In this case, SomeClass is an object with some relevant data.
We have the interface which is the bridge between Provider and Logic. It will have its importance later on in the test.
And then the logic where all the code is stored. We want to test MethodToTest, in which we are going to take an information from SomeClass and add someValue to it and return that value.
First thing, Logic does not know about Provider, it connects to it via the interface. We want to get some data from SomeClass, we will consider it has a member called Data returning an integer. We can now update the provider and the interface.
public class Provider : MonoBehaviour, IProvider
{
[SerializeField] private SomeClass m_someClass;
private Logic m_logic;
public int SomeClassData => m_someClass.Data;
void Start()
{
m_logic = new Logic(this);
}
}
public interface IProvider
{
int SomeClassData { get; }
}
You may think to pass the SomeClass object from the interface but doing it like this removes the dependency between Logic and SomeClass, they simply don't have any connection.
Now in Logic we can add the code:
public int MethodToTest(int someValue)
{
return m_provider.SomeClassData someValue;
}
We can now move to the test
[Test]
public void LogicTest_MethodToTest()
{
// this would likely be in the Setup method to reuse provider and logic
IProvider provider = Subsitute.For<IProvider>();
Logic logic = new Logic(provider);
// Use NUnit to have the empty IProvider to return a given value when
// SomeClassData is called (else it returns default 0)
// This means that when MethodToTest will call for SomeClassData, it will return 5
provider.SomeClassData.Returns(5);
int result = logic.MethodToTest(10);
Assert.AreEqual(result, 10);
}
Thanks to the Interface, we no longer need to create all kind of object, the test is limited and NSubstitute takes care of the mocking (creating an empty object of an interface).
Now, this is fine but only one test is not so good, so we can start adding more test. We can copy paste the Test, rename the method and change the value to check but this is redundant. Let's use TestCase instead.
[TestCase(0, 0, 0)]
[TestCase(5, 0, 5)]
[TestCase(5, 5, 10)]
public void LogicTest_MethodToTest(int someClass, int someValue, int expected)
{
// this would likely be in the Setup method to reuse provider and logic
IProvider provider = Subsitute.For<IProvider>();
Logic logic = new Logic(provider);
// Assign the given value for flexible testing
provider.SomeClassData.Returns(someClass);
int result = logic.MethodToTest(someValue);
// compare with expectation
Assert.AreEqual(result, expected);
}
Each value given in the test case will be passed as parameter to the method when NUnit calls it. Instead of static values, you can now run a set of test to make sure the method works in many cases. You should then add corner cases like negative values or max int and so on to fiddle with your method until all green.
In this context, we do not text the Unity part. Simply because we know it works. Unity did the tests already so there is no need to check if the input works or if Start is being called properly. Logic and the test rely on the fact that SomeClass would return specific values. In this case, we are only testing Logic so we assume SomeClass was implemented and tested properly so we don't need to test it here. The IProvider can mock values via the return method.
To sum it up: Remove all the logic from Unity class (Provider) and move them to a Logic class. Create a bridge between Unity class and logic via an interface (IProvider). Anything needed in Logic from Provider goes through IProvider so to remove any dependency.
In the test, create the mock IProvider and pass it to the newly created Logic object. You can start testing.
The real benefit is that you now know the method works and if you were to modify it later on, you have your test to confirm it still does it all right.