Hello, I'm Tom Wood

Logo

An aspiring tech designer, programmer and game developer

View My GitHub Profile

UNITY TEST RUNNER BLOG

by Thomas Wood

After spending time being introduced to and writing tests and testable code in Unreal Engine 4 (UE4) at Rare, I thought I would explore the testing framework of the engine I use the most at home - Unity. I picked a small scene with some scripts in it that I had written with no intention to test and got to writing a few tests in it while learning what Unity Test Framework/Runner has to offer. The scene I had to test was a simple 2D Asteroids-like shooter and the tests I decided to write centre around the bullet's movement. This functionality of the bullet is contained in the BulletMove component which runs this Move() function every frame:

void Move()
{
  Vector3 positionChange = transform.up * moveSpeed * Time.deltaTime;
  transform.position += positionChange;
}

It didn't take long to get started - the framework feels very familiar after working in UE4's. It also introduced me to Assembly Definitions in Unity for the first time, which seem like a must for compile times in large Unity projects a la UE4's modules. The first test I wrote for BulletMove was as follows:

[UnityTest]
public IEnumerator TEST_Bullet_Moves_Expected_Distance_GIVEN_Move_Speed()
{
  // Set-up bullet
  bullet = MonoBehaviour.Instantiate(Resources.Load<GameObject>("Bullet"));
  bulletMove = bullet.GetComponent<BulletMove>();
  bulletMove.moveSpeed = 10.0f;
      
  Vector3 initialPosition = bullet.transform.position;

  // Move bullet
  const float passedTime = 0.1f;
  yield return new WaitForSeconds(passedTime);
      
  Vector3 newPosition = bullet.transform.position;

  float moveMagnitude = (newPosition - initialPosition).magnitude;

  // Assert bullet moved expected distance with a small tolerance
  Assert.AreEqual(bulletMove.moveSpeed * passedTime, moveMagnitude, 0.1f);
            
  // Clean-up bullet
  bulletMove = null;
  Object.Destroy(bullet);
}

After making sure the test failed and then passed, I started on the second test. At this point it made sense to pull out the set-up and clean-up of the first test into new functions as I knew this would be the same for the both tests. Rather than manually calling the set-up and clean-up functions myself at the start and end of every test, I was glad to see Unity Test Framework/Runner had mark-up which would have the functions called automatically:

// Called before every test
[SetUp]
public void SetUp()
{
  bullet = MonoBehaviour.Instantiate(Resources.Load<GameObject>("Bullet"));
  bulletMove = bullet.GetComponent<BulletMove>();
}

// Called after every test
[TearDown]
public void TearDown()
{
  bulletMove = null;
  Object.Destroy(bullet);
}

I then wrote the second test, which makes sure the bullet moves in the correct direction based on it's rotation:

[UnityTest]
public IEnumerator TEST_Bullet_Moves_Expected_Direction_GIVEN_Rotation()
{
  bullet.transform.Rotate(Vector3.forward, 45.0f);
  bulletMove.moveSpeed = 10.0f;

  Vector3 initialPosition = bullet.transform.position;

  const float passedTime = 0.1f;
  yield return new WaitForSeconds(passedTime);

  Vector3 newPosition = bullet.transform.position;

  Vector3 moveDirection = newPosition - initialPosition;
            
  Assert.AreEqual(-0.7f, moveDirection.x, 0.01f, "X position");
  Assert.AreEqual(0.7f, moveDirection.y, 0.01f, "Y position");
  Assert.AreEqual(0.0f, moveDirection.z, "Z position");
}

Much nicer now we don't have the set-up and clean-up code in there! At this point the tests were taking longer than I'd like to complete, with having to yield for the Update() function to run being the main culprit. The two tests were taking 0.127s and 0.124s respectively. Most of that time is the 0.1s yield:

UnityTest Times

I wasn't a fan of the idea of reducing the yield or increasing the time scale just to get the test to run faster as I thought that was not representative of the scenes normal state. So, I took a look around and did a small refactor on my BulletMove code. Instead of making the Move() function access the global Time namespace itself I passed the delta time into the function as a parameter. This meant in the tests I didn't have to yield at all, I could just call Move() directly with whatever time I wanted to pretend had passed. At this point I could also make the tests use the normal NUnit Test attribute instead of UnityTest. The difference between the UnityTest and Test attributes are as follows:

With the yields gone from the tests their times were down to 0.05s and 0.001s:

Test Times

At this point I was happy with my tests and what I'd learned of the Unity Test Framework/Runner. Overall, at a glance, tests seem to work and have the capabilities I would have expected. I think if I was to continue writing tests in Unity I would use a combination of both UnityTests and Tests, as would be expected. I would definitely focus the full, yieldable tests on objects which regularly had issues or caused bugs, writing them in post and trying to keep them sparse so they wouldn't bloat the test times. I'd also test multiple things in a single UnityTest considering the times it can take - with this movement code for example I'd have a single test yielding so the Update() of the object is called, and then test both the move distance and direction were the expected results. It was also nice to see my experience writing tests at Rare coming into play as I felt very comfortable using Unity Test Framework/Runner. Nice experience and something I will be considering for future projects.