怎么使用unity测试游戏_使用Unity Test Runner测试测试驱动的开发

怎么使用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.

测试驱动开发(TDD)是在编写代码本身之前为一段代码编写自动化测试的实践。 在这篇博客文章中,我将解释我和我的同事如何使用TDD制作游戏(带有代码片段),以及什么做得好,什么没做。 TDD并不是万能的,但是我们无疑学到了很多东西,因此成为了更好的开发人员。 我们在项目中使用了Unity Test Runner ,该项目是一个用于在Unity中编写和执行NUnit测试的系统。

The usual workflow for TDD is as follows:

TDD的通常工作流程如下:

  1. First, you determine what the code will do. Let’s say that the code sets the value of codeHasRun from false to true.

    首先,确定代码将执行的操作。 假设该代码将codeHasRun的值从false设置为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.

    接下来,编写一个测试以检查代码是否已完成其工作。 在这种情况下,测试将检查codeHasRun等于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.

TDD通常用于软件开发中,但在游戏开发中却很少见。

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.

上个月,Unity发行工程小组中来自不同团队的五个人聚在一起,探讨使用TDD制作游戏。 我之前曾听说过,有些开发人员认为他们无法使用其游戏代码来开发自动游戏,而且他们当然不能使用TDD,所以我想亲自看看。

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).

我们决定制作一些经典游戏-Pong,Snake,Asteroids和Flappy Bird。 这样做的好处是我们不需要花任何时间来设计游戏玩法,因为我们已经对所有东西如何组合在一起有了一个粗略的想法(或者我们认为)。

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:

我们在Pong中将桨划破了以下属性:

  • A rectangle with a scale of (0.5, 2, 0)

    比例为(0.5,2,0)的矩形
  • Can move in in the XY plane

    可以在XY平面内移动
  • Cannot move in Z

    无法在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:

这样做为我们编写测试提供了良好的起点。 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
       [Test]
       public void AtLeastOnePaddleIsSuccesfullyCreated()
       {
           GameObject[] paddles = CreatePaddles();
           // Assert that the paddles object exists
           Assert.IsNotNull(paddles);
       }
       [Test]
       public void TwoPaddlesAreSuccesfullyCreated()
       {
           GameObject[] paddles = CreatePaddles();
           // Assert that the number of paddles equals 2
           Assert.AreEqual(2, paddles.Length);
       }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
       [ Test ]
       public void AtLeastOnePaddleIsSuccesfullyCreated ( )
       {
           GameObject [ ] paddles = CreatePaddles ( ) ;
           // Assert that the paddles object exists
           Assert . IsNotNull ( paddles ) ;
       }
       [ Test ]
       public void TwoPaddlesAreSuccesfullyCreated ( )
       {
           GameObject [ ] paddles = CreatePaddles ( ) ;
           // Assert that the number of paddles equals 2
           Assert . AreEqual ( 2 , paddles . Length ) ;
       }

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

从这些测试中,您可以看到我需要编写一个名为CreatePaddles的方法来创建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.

统一测试运行 包括以下功能,诸如 UnityTest 。 UnityTest返回一个IEnumerator并作为协程在Play模式下运行,这使您可以测试可能需要一帧或更多帧才能完成的动作。

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

在此示例中,我们使用UnityTest来检查桨板是否能够离开板子的边界。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[UnityTest]
       public IEnumerator Paddle1StaysInUpperCameraBounds()
       {
           // Increase the timeScale so the test executes quickly
           Time.timeScale = 20.0f;
           // _setup is a member of the class TestSetup where I store the code for
           //setting up the test scene (so that I don’t have a lot of copy-pasted code)
           Camera cam = _setup.CreateCameraForTest();
           GameObject[] paddles = _setup.CreatePaddlesForTest();
           float time = 0;
           while (time < 5)
           {
               paddles[0].GetComponent<Paddle>().RenderPaddle();
               paddles[0].GetComponent<Paddle>().MoveUpY("Paddle1");
               time += Time.fixedDeltaTime;
        yield return new WaitForFixedUpdate();
           }
           // Reset timeScale
           Time.timeScale = 1.0f;
           // Edge of paddle should not leave edge of screen
           // (Camera.main.orthographicSize - paddle.transform.localScale.y /2) is where the edge
           //of the paddle touches the edge of the screen, and 0.15 is the margin of error I gave it
           //to wait for the next frame
           Assert.LessOrEqual(paddles[0].transform.position.y, (Camera.main.orthographicSize - paddles[1].transform.localScale.y /2)+0.15f);
        }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[ UnityTest ]
       public IEnumerator Paddle1StaysInUpperCameraBounds ( )
       {
           // Increase the timeScale so the test executes quickly
           Time . timeScale = 20.0f ;
           // _setup is a member of the class TestSetup where I store the code for
           //setting up the test scene (so that I don’t have a lot of copy-pasted code)
           Camera cam = _setup . CreateCameraForTest ( ) ;
           GameObject [ ] paddles = _setup . CreatePaddlesForTest ( ) ;
           float time = 0 ;
           while ( time < 5 )
           {
               paddles [ 0 ] . GetComponent < Paddle > ( ) . RenderPaddle ( ) ;
               paddles [ 0 ] . GetComponent < Paddle > ( ) . MoveUpY ( "Paddle1" ) ;
               time += Time . fixedDeltaTime ;
         yield return new WaitForFixedUpdate ( ) ;
           }
           // Reset timeScale
           Time . timeScale = 1.0f ;
           // Edge of paddle should not leave edge of screen
           // (Camera.main.orthographicSize - paddle.transform.localScale.y /2) is where the edge
           //of the paddle touches the edge of the screen, and 0.15 is the margin of error I gave it
           //to wait for the next frame
           Assert . LessOrEqual ( paddles [ 0 ] . transform . position . y , ( Camera . main . orthographicSize - paddles [ 1 ] . transform . localScale . y / 2 ) + 0.15f ) ;
         }

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.

如果我在这里使用deltaTime,则测试可能会不稳定,因为它可能会有所不同 。 我设置了Time.captureFrameratefixedDeltaTime ,以使其可预测。 如果我没有在开始时设置Time.timeScale,则该测试将花费5秒钟以上的时间来完成–这对于一项测试是不可接受的! 将timeScale设置为20意味着时间比平常快20倍。 这意味着5秒的测试可以在大约0.25秒内执行。

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.

在上面的测试中,我模拟了将操纵杆向上移动5秒钟。 该测试检查是否通过MoveUpY方法限制了桨板从板上移出。 当我第一次编写测试时,MoveUpY没有阻止桨从板上移开的功能。 这意味着测试失败。

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!

在编写测试之后但在添加实际功能之前,检查测试是否真正失败非常重要,否则可能会导致误报。 在项目的早期,当我仍然掌握所有内容时,我编写了一个测试,忘了检查它是否失败,然后注意到即使该功能在游戏中被破坏,这些测试仍在通过中。 当我回去检查我的测试时,我意识到自己写错了(实际上我忘了调用我正在测试的方法了……)! 这很尴尬,但是我吸取了教训,并确保将来在编写功能之前检查测试失败。 Unity Test Runner允许您重新运行失败的测试,如果项目中有很多测试,这可以帮助加快迭代时间!

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!

使用TDD可能会有点慢,但是一旦开始使用,这是一种非常有意义的工作方式。 这也意味着项目后期的更改更快,更安全!

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.

这种工作方式还帮助我们确定了不同系统在游戏中的工作方式。 当我们重新创建我们已经熟悉的游戏时,我们不确定设计中的各个系统如何组合在一起。 编写测试首先意味着我们可以计划我们希望事情如何工作,然后将它们付诸实践以查看它们是否切实可行。 如果需要,我们可以从那里进行迭代。 我还发现使用TDD可以使我编写更好,更整洁的代码,因为我将更多的精力投入到正在编写的内容及其原因中。

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.

请记住,TDD并不是万能的,但它是一个很好的安全网,当所有测试完成后,它可以在项目中更快地迭代,这一点非常重要。 看到所有绿色的刻度都出现在Unity Test Runner窗口中也是一种令人非常满意的感觉。 但是,使用TDD并不意味着您不需要任何其他类型的测试。 TDD是一种质量驱动的开发方法,而不是QA策略。

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

感谢我的同事Marc Di Luzio,Ugnius Dovidaukas,LinasRatkevičius和Andy Selby在这个项目上与我合作!

翻译自: https://blogs.unity3d.com/2018/11/02/testing-test-driven-development-with-the-unity-test-runner/

怎么使用unity测试游戏