Search Unity

テスト駆動開発(TDD)とは、コードそのものを書く前にコード部品の自動テストを書くプラクティスのことです。本ブログ記事では、私が同僚たちと、ゲーム開発においてどのように TDD を使ったかをコード例を示しながら説明し、うまくいったことも、うまくいかなかったことも両方お伝えしようと思います。TDD はすべてを解決してくれる手法ではありませんが、結果として私たちは多くのことを学び、そしてよりレベルの高い開発者になれたと思っています。なお私たちのプロジェクトでは、Unity Test Runnerを使いました。このツールは、Unity で NUnit テストの記述と実行を行うためのシステムです。

一般的な TDD のワークフローは次のようなものです。

  1. まず、コードが何をするかを決めます。たとえば、codeHasRun の値を false から true にするコードを書くとします。
  2. 次に、コードが正しく動作したかチェックするテストを書きます。この場合は、テストは codeHasRun が true であるかチェックします。
  3. テストを実行します。コードがまだ書かれていないので、テストは当然失敗します。
  4. コードを書きます。
  5. テストを実行します。今度はテストが成功するはずです。

このワークフローに従えば、コードをリファクタリングして変更を加えるプロセスをスピードアップできます。なぜなら、何が壊れていて、なぜそれが壊れているかが明確にわかるからです。

なぜコードそのものを書く前にテストを書くのか、不思議に思う方もいらっしゃるでしょう。その理由は、コードを書いてからテストを書くようにすると、開発者はしばしばテストが通るようにテストを書いてしまうことがあるからです。まず失敗するテストを書けば、テストが失敗するのには妥当な理由(たとえば、必要な機能が正しく実装されていない)があることを確信でき、誤検知を排除することができます。

TDD はソフトウェア開発で広く活用されていますが、ゲーム開発での活用例は稀です。

この記事を書いた前の月に、Unity のリリースエンジニアリンググループ内のさまざまなチームから 5 人が集まって、TDD を活用したゲーム制作について調査を行いました。私は以前、自分たちのゲームコードを使って自動化されたゲームを作ることはできず、TDD を使うこともできないと考える開発者がいると聞いていたので、それが本当かを自分の目で確かめてみたいと思ったのです。

私たちは、Pong、Snake、Asteroids、Flappy Bird など、古典的なゲームを作ってみることにしました。そうすれば、ゲームプレイをデザインする時間を省けるし、それぞれのゲームがどのように動くか(少なくとも、どのように動くかのアイデア)の大体のイメージはつけることができたからです。

しかし、それぞれのゲームがどのような感じであるか知っていても、テストの組み立て方を決めるには、やはりコンセプトを深く検討していく必要がありました。Pong を例にとれば、私は「パドルが動く」ことは知っていましたが、そもそも「パドル」がどのように作られているのか、十分に説明できませんでした。このように、すべてのアイデアを細かく検討していく必要があったのです。

私たちは、Pong のパドルを次のような属性に分解しました。

  • スケールを (0.5, 2, 0) に設定した長方形である
  • XY 平面上を移動できる
  • Z 方向には動けない
  • 左右には動けない
  • 画面外には移動できない
  • コリジョンがある
  • Kinematic な Rigidbody である

このように属性へと分解することで、どのようにテストを書き始めればよいかが明確になりました。テストは例えば次のようになります。

これらのテストから、私は GameObject を 2 つ格納した配列を生成する CreatePaddles というメソッドを書く必要があるとわかります。

Unity Test Runner には、UnityTest などの機能も搭載されています。UnityTest は、IEnumerator を 1 つ返し、再生モードではコルーチンとして実行されます。完了までに 1 フレーム以上必要なアクションをテストするために使うことができます。

この例では、パドルが盤面の端を超えていかないかチェックするために UnityTest を使いました。

ユニットテストはある機能を最小限に細かく分割した部品をテストするように書くべきであり、そのため、私は片方ずつのパドルについて、盤面の上端と下端の両方にある場合をテストしました。

もしここで deltaTime を使ったら、deltaTime の値は一定ではないため、テストは不安定になります。私はTime.captureFramerate や、Time.fixedDeltaTime を設定して、フレームにかかる時間を予測可能なものにしました。また、最初に Time.timeScale を設定しない場合は、テスト完了までの時間が 5 秒を超えてしまいました。これは 1 つのテストにかける時間としては受け入れられない数字です。timeScale の値を 20 に設定すると、ゲーム内の時間は通常の 20 倍速く進むようになり、5 秒かかるテストも約 0.25 秒で完了できるようになります。

ここまでに示したテストは、パドルを 5 秒間上方向に動かす動作をシミュレートします。テストはパドルが MoveUpY メソッドを使って動かされるとき、盤面の端を超えていかないかをチェックします。最初にテストを書いたときは、MoveUpY にはボードを超えていかないようにパドルを止める機能がなかったため、テストが失敗しました。

テストを書いた後、実際の機能を実装する前の段階では確かにテストが失敗する、つまり最初は誤検知してしまうことを確認することは非常に重要です。このプロジェクトを始めてすぐの頃、テストが最初に失敗するかチェックしなかったために、ゲームの中である機能が壊れているのにテストは通ってしまう状態になったことがありました。そして自分の書いたテストをもう一度見直すと、テストを間違えて書いていたことに気が付いたのです(なんと、テストしているメソッドを呼ぶのを忘れていました)。この時はすごく恥ずかしい思いをしましたが、これを教訓として、そのあとは機能を実装する前にちゃんとテストが失敗することを確認する習慣がつきました。Unity Test Runner には失敗したテストを再実行する機能もあるので、プロジェクトに大量のテストがある場合も、素早くイテレーションを回すことが可能です。

TDD は、導入しはじめのときは少し作業速度を下げるかもしれませんが、習熟すると、作業上で大きな見返りが期待できる手法です。また、あとからプロジェクトに修正を加えるときも、より素早く、より安全に修正をすることができます。

また、このような作業スタイルは、ゲームの中でさまざまなシステムがどのように働くのかを理解する助けにもなりました。私たちはよく知っているゲームを真似して作ろうとしたのですが、それでもゲームの設計に入っているシステムがそれぞれどのように組み合わさるのかははっきりわかりませんでした。最初にテストを書くことで、システムの中にあるものをどのように動作させるかを計画することができ、実践を通してそれが本当に可能であるかを検証することができました。また、必要に応じてコードを書き直して試すこともできました。私自身についても、TDD を使うことで、何を書こうとしていて、なぜそれを書こうとするのかをより深く考えるようになったため、より良い、より整ったコードを書くようになったことに気づきました。

TDD がすべてを解決する手段ではないことは認識しておく必要がありますが、セーフティーネットとして非常に優秀な手段であり、また、プロジェクトで一度テストを整備されれば、イテレーションの速度を上げることも可能になります。また、Unity Test Runner ウィンドウに表示されたすべてのテストに、緑のチェックマークが付いていく様子は見ていて気持ちのいいものです。しかし、TDD を使えば他のテストは必要ないというわけではありません。TDD は品質駆動の開発アプローチの 1 つであり、QA 戦略ではないためです。

最後に、このプロジェクトに一緒に取り組んでくれた、同僚の Marc Di Luzio、Ugnius Dovidaukas、Linas Ratkevičius、Andy Selby に感謝の念を示します。

17 コメント

コメントの配信登録

コメント受付を終了しました。

  1. This is great, but the hardest part, IMO is the implementation of CreatePaddlesForTest. How do you do that? Is this repository publicly available?

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

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

  4. 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. Sophia Clarke

      11月 6, 2018 1:05 pm

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

  5. 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!

  6. 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. Sophia Clarke

      11月 6, 2018 2:53 pm

      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!

  7. software testing

    11月 3, 2018 10:06 am

    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.

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

  9. We need code coverage reports.

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

  11. Ippokratis Bournellis

    11月 2, 2018 3:59 pm

    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. Sophia Clarke

      11月 6, 2018 11:28 am

      We can definitely look into this :)

  12. Niklas Werner

    11月 2, 2018 3:50 pm

    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. Sophia Clarke

      11月 2, 2018 3:58 pm

      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. Niklas Werner

      11月 2, 2018 11:50 pm

      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!