Search Unity

Test-driven development (TDD) is the practice of writing automated tests for a piece of code before writing the code itself. In this blog post, I’m going to explain how my colleagues and I used TDD for making games (with code snippets), as well as what went well and what didn’t. TDD is not a fix-all, but we definitely learned a lot and became better developers as a result. We used Unity Test Runner in our project, which is a system for writing and executing NUnit tests in Unity.

The usual workflow for TDD is as follows:

  1. First, you determine what the code will do. Let’s say that the code sets the value of codeHasRun from false to true.
  2. Next, you write a test that checks that the code has done its job. In this case, the test would check that codeHasRun equals true.
  3. Run the test. The test should fail because the code hasn’t been written yet. If the test doesn’t fail, there’s something wrong with the test, or with your understanding of the code.
  4. Write the code.
  5. Run the test. The test should now pass.

Following this workflow speeds up the process of refactoring code and making changes, because you can see straight away what has broken and why.

You may wonder why we write the test before writing the code itself. This is because writing tests after writing the code can often lead to developers writing tests to make them pass. When you write a failing test first, you’re making sure that it fails for a good reason (such as not implementing the required functionality correctly), as well as ruling out false positives.

TDD is commonly used in software development, but it’s quite rare in game development.

Last month, five people from different teams in Unity’s Release Engineering Group got together to look into making games using TDD. I’d heard before that some developers think they can’t have automated games with their game code, and they certainly couldn’t use TDD, so I wanted to see for myself.

We decided to make a few classic games — Pong, Snake, Asteroids, and Flappy Bird. The benefit of this was that we didn’t need to spend any time designing gameplay, because we already had a rough idea of how everything would come together (or so we thought).

While we knew how each game would work, we still had to really drill down into the concepts so we could know how to structure our tests. With the example of Pong, I knew that a paddle should move… But what even is a paddle? Every idea had to be broken down.

We broke a paddle in Pong down to the following attributes:

  • A rectangle with a scale of (0.5, 2, 0)
  • Can move in in the XY plane
  • Cannot move in Z
  • Cannot move left and right
  • Cannot move outside the bounds of the screen
  • Has collision
  • Is a Kinematic Rigidbody

Doing this gave us a good starting point for writing tests. For example:

From these tests, you can see that I need to write a method called CreatePaddles that creates an array of 2 GameObjects.

The Unity Test Runner includes functionality such as a UnityTest. A UnityTest returns an IEnumerator and runs in Play mode as a coroutine, which allows you to test actions that may need a frame or more to complete.

In this example, we used a UnityTest to check that the paddles could not leave the bounds of the board.

A unit test should test the smallest piece of functionality possible, so I tested each paddle for both the top and bottom of the board.

If I was to use deltaTime here, the test would be unstable because it can vary. I set up Time.captureFramerate, or fixedDeltaTime, to make this predictable. If I didn’t set Time.timeScale at the start, the test would take over 5 seconds to complete — which is unacceptable for one test! Setting timeScale to 20 means time passes 20 times quicker than normal. This means a 5-second test can execute in roughly 0.25 seconds.

In the above test, I simulate moving the paddle upwards for 5 seconds. The test checks that the paddle is restricted from moving off the board by the MoveUpY method. When I first wrote the test, MoveUpY didn’t have the functionality to stop the paddle from moving off the board. This meant that the test failed.

It’s really important to check that your test actually fails after you write it but before you add the actual functionality, or you could end up with false positives. Early on in the project, while I was still getting the hang of everything, I wrote a test, forgot to check that it was failing, then noticed that the tests were passing even though the functionality was broken in the game. When I went back to check my test, I realized I’d written it wrong (I’d actually forgotten to call the method I was testing…)! It was embarrassing, but I learned my lesson and made sure in future to check for test failures before writing the functionality. Unity Test Runner allows you to rerun failed tests, which can help speed up iteration time if you have lots of tests in a project!

Using TDD can be a bit of a slow start, but once you get going it’s a very rewarding way of working. It also means that changes later in the project are quicker and safer!

This way of working also helped us to shape how different systems would work within the game. While we were recreating games that we were already familiar with, we weren’t sure how individual systems within the design would come together. Writing the tests first meant that we could plan out how we wanted things to work, and then put them into practice to see if they were actually feasible. We could iterate from there if we needed to. I also found that using TDD caused me to write better and neater code because I was putting more thought into what I was writing and why.

It’s very important to bear in mind that TDD is not a fix-all, but it’s a nice safety net, and it allows for faster iteration in the project once all the tests are in place. Seeing all the green ticks appear in the Unity Test Runner window was a really satisfying feeling, too. However, using TDD doesn’t mean you don’t need any other kind of testing. TDD is a quality-driven development approach, rather than a QA strategy.

Thanks to my coworkers Marc Di Luzio, Ugnius Dovidaukas, Linas Ratkevičius, and Andy Selby for working with me on this project!

Добавить комментарий

Вы можете использовать эти теги и атрибуты HTML: <a href=""> <b> <code> <pre>

  1. I’d like to see how tests are done in more complex environments. For example, lets test a nework prediction and delay compensation algorithms working together and see if players stay close enough to their server positions while the network delay stays in defined range. My point is, for games we need a completely differrent tools for TDD because it’s not some kind of computation or UI program, but a complex realtime simulation.

  2. 你这个东西有问题,我觉得不行。

  3. Hi!
    This article is focused on ingame testing (and I totally understand why :) )
    But as a dev who is working on an editor extension, I felt that testing for an editor extension is quite difficult. Without talking about user input/ CI testing (means in batch mode), I just couldn’t find a way to have my tests seen by unity’s test tools. It required asmdef files but I couldn’t succeed in getting it to work properly. But I do know we could use unit testing because we tend to break things :)

    Is there any plan to improve Unity testing tools towards that kind of usage ?

    1. Hello,

      It’s hard for me to say what’s happening without knowing your project, but what do you mean when you say you couldn’t get the asmdef to work properly?

      I’ll upload my project to github so you can have a look at how I handled the asmdefs :)

  4. Good to know! However, I think that this work perfect for small games. But when you start a big project, Unity test runner lack of flexibility. I would like to have an update on it!

  5. Thank you for the post. I was thinking about how to integrate at least some TDD approach to Unity for some time as I was feeling like your developer friends.

    What I’m curious about is setting up the scenes. Plenty of things in the scene are setup manually or dynamically, if not procedural, and can change with gameplay. Trying to say it feels pretty chaotic to setup a test environment. This makes harder to imagine a setup class like TestSetup. Are there any approaches you can suggest here that may be helpful?

    1. Hello,

      My main suggestion is that if it seems «chaotic», maybe your tests are too complicated! :D

      Try to strip it down to simpler chunks and see how that works :) This is one of the reasons that it’s easier to do this on a new project. Good luck!

  6. Nice! Thanks a lot. The way you presented whole concept is crystal clear. I read your full blog on Testing Test-Driven Development and I found it was very informative, and helped me a lot. I always look for blog like this with which I can enhance my skills.

  7. Nice post. I always like reading about unit testing with Unity.

    The main problem is that these tools don’t seem to evolve really. The test runner (framework and editor window) never receive any updates, and that is a shame since there is ALOT to improve, to make it work well for «real world» projects.

    I posted this forum thread a while ago with some of my feedbacks on using the test runner framework: https://forum.unity.com/threads/feedback-for-logassert-class.530539/

    I have collected a few other suggestions since then, will be happy to share them (would be really glad to speak to someone from that team actually.

    I met with Harald at Unite Berlin and spoke to him briefly about some of my feedbacks, but would be happy to share them again and to help move this forward (for example — submitting fixes as PRs if you open source some of the code).

  8. We need code coverage reports.

  9. Is there any way to use another assertion library like Fluent Assertions (https://fluentassertions.com/)? And if we could, would the optimal way be to load up the Unity .sln file in Visual Studio and add it via Nuget, since Unity does not support using Nuget in the Editor.

  10. Interesting, any chances to share your projects via github? It is good to read about a concept and better to see how the dev implemented it.

    1. We can definitely look into this :)

  11. I have looked into the test runner myself in the past days, but found it now very useful. From what I have seen when executing a test then a new empty scene is being created, so you would need to create all the components and gameobjects in the unit test manually. I have noticed that you have a setup class for that, but this is only going to work for a simple paddle in a ping pong game. I’m working on a complex project for months now and some prefabs have tons of childs and components and there is no way to manually create this in a unit test. The test runner would be useful when I could create a unit test scene by hand including all environment and then execute the unit test scene or at least instantiate prefabs in the unit test scene, but I couldn’t get my finished prefabs in a variable in the unit test. Do you have any advice for doing complex unit tests like spawning two fully animated meshes with lots of childs and check for example a shoot function with and without obstacles in the way?

    1. Hello! Thanks for reading.

      It depends on what exactly you’re testing, but from your comment, it sounds like you need an integration test rather than a unit test. However, you could still achieve what you’re after with the Unity Test Runner by loading in your own scene for your tests with SceneManager.LoadScene https://docs.unity3d.com/ScriptReference/SceneManagement.SceneManager.LoadScene.html

      It may also be worth looking at PrebuildSetupAttriubute which you can read about here: https://docs.unity3d.com/Manual/PlaymodeTestFramework.html

      I hope this helps :)

    2. Thank you, but I think it is not possible to load a new scene with SceneManager.LoadScene inside the unity test runner, but I have not yet tested it myself. I have searched for integration tests and it seems to indeed be what I’m looking for. Are there any official resources for integration tests inside unity? I could barely find any information on except one short explanation on bitbucket. Would be interesting to read or watch more about it!