0

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

Blue
  • 820
  • 4
  • 17
  • 1
    Does this answer your question? [What is a NullReferenceException, and how do I fix it?](https://stackoverflow.com/questions/4660142/what-is-a-nullreferenceexception-and-how-do-i-fix-it) – Max Play Dec 07 '22 at 18:17
  • Not quite, I found that I could use something called "mocking" perhaps, but does that mean I just copy the ProcessMovementOfShip function over to the test script? if so, how do I verify that the line: "_rigidBody.MovePosition(delta);" moves the character? the rigidbody2d is missing, how do I mock a rigidbody? – Fredrik Olofsson Dec 08 '22 at 08:44

1 Answers1

0

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.

Everts
  • 10,408
  • 2
  • 34
  • 45
  • I am still pretty lost, I tried asking a friend for help and he just sat there mocking me: "why are you trying to test stuff?" I tried telling him that it's pretty useful to learn, but man, I have been on this topic for the last few days and I am still not that much closer to a solution. I will look into this topic at a later date, perhaps it makes more sense after I have learned more about unity and c# Thanks for the help though! I will come back to your answer now and then! have a happy Christmas – Fredrik Olofsson Dec 09 '22 at 14:41
  • Many large scale projects go without unit test. It's valuable but not necessary. Testing if a+b=c feels useless, test is more valuable with complex logic. Especially because it tells you the method works without the necessity to run the game. Sometimes, it gets really difficult to test a specific setup in game, unit test does it easy. – Everts Dec 09 '22 at 17:33