イベント

2019.11.08

【Unite Tokyo 2019】「Unity Test Runnerを活用して内部品質を向上しよう」セッションレポート

2019年9月25~26日にグランドニッコー東京において、技術カンファレンスイベント「Unite Tokyo 2019」が開催されました。

本記事では、9月26日に実施されたセッション「Unity Test Runnerを活用して内部品質を向上しよう」の内容を一部抜粋して紹介します。

ゲーム開発でもテストは重要

本セッションでは、Unityにおいて、ユニットテストを実行する「Unity Test Runner」の仕組みを活用して開発者の手でテストコードを書き、ゲームの内部品質を向上するためのベストプラクティスやTipsなどが紹介されました。

登壇した株式会社ディー・エヌ・エー システム本部 品質統括部 品質管理部 SWETグループに所属する長谷川 孝二は、おもにUnity向けのテスト自動化支援に携わっています。

ディー・エヌ・エー システム本部 品質統括部 品質管理部 SWETグループ 長谷川孝二

なお、本セッションの前提知識として、2019年9月開催の「CEDEC 2019」において講演された「組織にテストを書く文化を根付かせる戦略と戦術」にて、タワーズ・クエスト株式会社の和田卓人氏から、開発者がテストコードを書く必要性や、開発効率を上げる根拠、取り掛かるべき指針が数字で公開されており、その点は本セッションでは割愛しているため、ぜひ参照してほしいと紹介されました。

SWETグループとは

DeNAのシステム本部に設立されている部署「SWET (Software Engineer in Test) 」グループは、他の横断的組織と連携して、テストの自動化のアドバイザリー、テストや検証ツールの作成、CI/CD、デバイスファームの利用、形式手法によって仕様書記述の品質を上げるといったR&Dの取り組みも担当しています。

テストに関する4つの誤解

ここからは、ゲーム開発におけるテストに関して、一般的に感じられているような「誤解」を解くための説明がなされました。

【誤解1】「テスト」==「デバッグ」

おもにゲーム業界の開発現場で使用されている「デバッグ」とは、バグの発見、原因箇所の特定、修正のことを指しますが、ソフトウェア開発全般で使われる「テスト」の目的は、バグの発見だけでなく、対象ソフトウェアの品質レベルが十分かの判断や、バグの作り込みを防ぐことを意味します。

その中でも「対象ソフトウェアの品質レベルのチェック」に関しては、正しく動作することを確認するとともに「クラッシュしないか」「ユーザーが実際に遊んで問題ないか」など品質を測る、という意味を持ちます。

また「バグの作り込みを防ぐ」ことに関しては、リグレッション(デグレ・エンバグ)をテストをすることにより、時間差なくバグを迅速に発見して修正することが可能とのことです。

【誤解2】ビルドしたゲームを、手動で操作するのがテスト

次の誤解ポイントとして、ビルドしたゲームを実機でテストすることだけがテストだと思われていることが挙げられました。

ゲームを遊べる形にビルドする以前に実施するテストでは、コンポーネントやメソッドのような小さな単位で早期に確実に検証できるのが、低レベルで可能なテスト(ユニットテスト・インテグレーションテスト)のメリットです。

もちろん、この段階では実機(手動)では操作できないので、必然的に自動テストが採用されます。

ユニットテストの利点は、素早く実行してバグを早期に発見できること、個々の部品の品質を上げておくことです。

また、再現の難しい条件を作り出しやすく、タイミング的に難しかったり、乱数的に低確率であったり、画面操作では指定できない値などに対してテストできることも、特にメリットになると長谷川は述べました。

【誤解3】テストを書けば品質が上がる

続いての誤解ポイントは、「テストを書けば品質が上がる」ことです。テストでは品質を測るだけで、実際は正しい設計やプログラミングで品質は上がります。

品質の低いプロダクトはテストが書きにくい事実もあるようです。「テスタビリティ」と呼ばれるテストのしやすさや、書きやすさについての品質特性を評価する観点のひとつがあります。

テストしやすいコードは、その時点でバグも少なく、可読性も高いく、すでに一定の品質を備えていると言えることがほとんどです。

この関係は「ルンバビリティ」という言葉に例えることができると長谷川は話しました。これはお掃除ロボットが自動で掃除するには、その部屋がある程度片付いていなければ、ロボット自体が動けずに、うまく掃除ができない、という意味で使われる言葉とのこと。

また、テスタビリティを含め、外部からのテストではわからないコード自体の品質のことを「内部品質」と呼び、保守性や可読性、移植性などがこれに含まれます。内部品質を高めていくには、ユニットテストを書き、プロダクトコードを見直すことが有効なようです。

【誤解4】テストコードは、プロダクトを開発した後から書く

最後に挙げられたのは、テストコードをプロダクト開発後に後付けで書けばいい、という誤解です。

やはりプロダクトを開発しながら、並行してテストコードも書き、常に実行していくことがベターだと長谷川は述べています。

ユニットテストは人間の手で作業するテストと違い、実行時間は短いので、書いたコードをリポジトリにコミットする流れの中で、テストコードを実行することを習慣化することが大事なようです。

長谷川の持論として「テストコードは建築における足場」だと話しました。実際の建築現場では建物が完成したら足場は撤去しますが、ソフトウェア開発ではリリースに含まれなければ良いので撤去する必要はなく、増改築でも引き続きその足場を利用する、という考え方です。

Unity Test Runnerを使ってみよう

「Unity Test Runner」とは、Unity標準のユニットテスト実行環境で、Unity2019.2からはPackage化され「Unity Test Framework(UTF)」となります。

実行環境としては、UnityエディタからEditMode/PlayMode/実機の各テストを実行できるだけでなく、コマンドラインでも実行可能です。またEdit Modeテストは、JetBrains Riderからも実行可能になっています。

Unity Test Runnerウィンドウは、Unityエディタのメニュー内、Window>General>Test Runnerで起動、EditModeもしくはPlayModeを選択してRun Allすると成否がカラーで表示されます。

2種類のテストモードがありますが、できる限りEditModeを使用することを推奨とのこと。EditModeではエディタ上で素早くテスト実行することができ、PlayModeはUnityエディタのプレイモードと同じ環境で実行することができます。

EditMode Tests

EditModeでは、テストコードの置き場所が2通り指定できます。従来はEditorフォルダの下に配置するしかなかったのですが、Unity 2018以降は任意のフォルダにAssembly Definition Fileを配置して、Editorアセンブリとして認識させることが可能です。

Assembly Definition Fileの設定内容は、テスト対象のアセンブリへの参照、Test AssembliesおよびPlatforms>EditorのチェックボックスをONにします。

テストクラスに制約はありませんが、テスト対象クラスと粒度を合わせることが慣例になっており、メソッドに[Test]アトリビュートをつけたものが、テストメソッドとして認識される仕様になっています。

セッションでは、テストメソッドなど、EditMode Testsのコード例も紹介されました。テストメソッドは、もっとも重要な検証(Verify)のコードから書き、下から上に、テスト対象の実行(Exercise)、環境設定(Setup)の順に書いていくのがおすすめとのことでした。

[UnityTest]アトリビュート

このアトリビュートは、複数のフレームにまたがるテストを記述できるもので、EditModeではEditor Application.updateコールバックループで実行されますが、yield returnにはnullしか指定できないなどの制約もあります。

PlayMode Tests

Edit Mode Testsとは別アセンブリを定義します。設定の違いは、Platforms > Editor をoffにするというところです。

注意事項として、テスト用の空のSceneファイルが生成・ロードされるため、テスト実行ごとにオーバーヘッドが必要となり、やや時間がかかります。またUnityエディタがクラッシュすると、Sceneファイルが残ってしまうことがあるとのことです。

また、一連のテスト実行の際には、生成されたSceneは使い回されるため、一個のテストメソッドを実行するたびに適切にクリーンナップしないと、後続のテストに影響します。

PlayMode Testsでは、Sceneベースのテストを書くことが大変なため、インテグレーションテストを書くのであれば、Poco、AltUnityTesterなどサードパーティのライブラリ導入を検討すべきと長谷川は述べました。

ゲーム開発向けユニットテストパターン

ここから本セッションの本題とも言える、ユニットテストのパターンや、ベストプラクティスの話題に入りました。

テストとはどう考えるべきか、テストの基本は「入力」と「出力」であり、観点によって入出力の捉え方が変わり、入出力はそのメソッドの何をテストするかによって定まります。

例えば実行速度が観点のときには、普通のパラメータを与えてレスポンスを見るのではなく、実行時間を図ったり、FPSを計測することが「出力」となります。

特に重要なのが、正しい入出力の見極めです。例えば出力として「セーブしました」というメッセージが出たため、テストはOKと判断してしまうと、実はメッセージは出したけど内部的に保存はされていなかった、もしくは保存データが壊れていた、というバグを見落とす恐れがあります。

価値の高いテストを書く

ショーストッパーと呼ばれる、基本的な操作ができなくなり、ほかのテストがすべて止まってしまうような部分、また、課金周りやクレームに直接繋がる部分はリスクが高く、価値の高いテストと言えます。

また、あまりプレイヤーが触らないような通らないルートや画面に関するテストも、見落としを防ぐ意味で価値のあるテストと言えます。

逆に価値に対してコストが高なってしまうものとして、副作用的なもの、目に見えるようなエフェクトやSEなどを挙げました。これらはあえてテストを書かないという選択もあるようです。

組み合わせ条件を減らす

テストの「入力」にあたる要素が多くなり組み合わせ条件が増えれば、テストもそれだけ複雑になります。例えば「弾がヒットして敵が破壊されたかどうか」の粒度でテストをすると、敵のHPや防御力などさまざまな要因が関係し、当然組み合わせの条件も増えてしまいます。

それを避けるため、プロダクトコード側の責務を適切に分割することで、個々のコンポーネントやメソッドが扱う要素が減り、組み合わせ数も減ります。

責務を分けたらコールスタックが増えて性能が出ないのでは?

実は本当にボトルネックになる部分は少ないと言われており、見極めはかなり重要になるようです。コンパイラの最適化を信じる、引数にrefをつけて参照渡しにするなど、対処方法は多数あると長谷川は述べました。

パフォーマンステスト

パフォーマンステストは、ユニットテストの段階から意識することが重要で、特にUpdateメソッドで動作する、呼ばれる頻度の高い部分を、できるだけ小さい規模のうちに意識することが大事だとのことです。

そのためには、メソッド単位に実行時間を測定して遅くなったら失敗するテストを書いたり、PlayModeでfpsを測定するなどの手段を使うと良いようです。

ただ、実行時間は環境に依存するため、実行時間のしきい値をピーキーに設定することはせず、自分以外の人がそのコードを修正したときに気をつけてもらうための「魔除けのお守り」程度の気持ちで考えると良いそうです。

また、メソッドの実行時間ではなく、AllocatingGCMemoryによって意図しないヒープメモリの確保が行われていないことを確認するテストも有効です。

他のオブジェクトへの依存

依存オブジェクトとは、テスト対象が内部で使用している他のオブジェクトのことです。例えば、乱数を内部に発生させている場合に、乱数の結果をテストコード側がコントロールできないと、テスト結果が正しいのかどうか判定できなくなってしまいます。

また、依存オブジェクトからの戻り値でテスト結果に影響するものを「間接入力」、依存オブジェクトに渡した引数のうちテスト結果として評価しなければならないものを「間接出力」と呼ぶそうです。

依存オブジェクトを持つメソッドをテストするための「テストダブル」といったパターンも存在します。これは映画のスタントや影武者のような役割を持っている手法になります。

テストダブルパターンは、あらかじめテスト側から依存オブジェクトのダブルを生成しておき、テスト対象は依存オブジェクトを内部で生成するのではなく外から受け取れるようにします。そうすることでテストダブルが間接入力を操作し、間接出力を検証します。

仕様変更のたびにテストが壊れる

よくある誤解としては「仕様変更があるからテストは書けない」のは間違いであり、「仕様変更があるからテストで保護しておく」という考え方に変えることが大事だとのこと。

将来の仕様変更を想定することで実装が複雑になり、品質も落ちてしまうったが仕様変更は起こらなかった、という経験はないでしょうか。テストがあることで仕様変更をしてもプロダクトが意図しない振る舞いになっていないか、すぐに気づくことができます。テストコードがあることによって「今必要でないことはしない」こと(YAGNIと言うそうです)を実行しやすくなるのです。

「なぜテストコードが壊れるか?」という話について、仕様ではなく実装のテストを書いてしまうことが要因とされます。ひとつの仕様に対して実装は何パターンも存在するため、実装が変わっただけで壊れやすいテストになってしまいます。

壊れにくいテストを実現するためには、副作用は検証しない、あえて定石から外れるといった選択肢もあります。

例えば「敵のHPゲージが5割を切ったら黄色にする」という仕様に対し、テストの定石としては50%と49%をテストするものですが、あえて80%と40%で色の変化にのみ着目するなど、境界値を攻めすぎず、十分役割を果たすテストにすることもできます。

また、メンテナンスが難しくなったテストコードは捨ててしまうこと、TDDではその過程で過剰にテストコードを書いてしまいがちなので、必要ないものは早めに取り除いても良いようです。またテストコードもプロダクトと同じように構造化することで、壊れにくいコードになります。

ここで長谷川は、Jon Reid氏の言葉「テストコードはガラスのような壊れやすいものではなく、竹のようにしなやかで柔軟性の高いものを目指すべき」と挙げ、アジア圏の建築現場で良く使われている竹の足場のように、安くて軽く、使わなくなったら捨てればいいようなコードと、思想は似ていると述べました。

テストを書く文化を根付かせる試み

最後に、SWETグループが採用したアプローチに関して、社内で横断的に共通的フレームワークのリファレンス実装に対して、テストコードのサンプルを書いて新種のバグを摘出し、タイミング良く、他の部署への引き合いに繋がったことが明かされました。

また、ゲーム領域でボトムアップでテストを書くことを始めるのは難しいため、いつでもチャンスが来たら、テストを普及できるような体制を整えておくことが大事とのことです。

長谷川は「米沿岸警備隊の格言 ”Senper Paratus(常に備えよ)”に表されるような心構えを大切にしながら、本活動に興味があればぜひ声をかけてほしい、そしてスライドには本セッションで紹介できなかったUnity Test RunnerのTipsを記載しているので、ぜひ参考にしてください」とのコメントでセッションを締め括りました。

取材・文・撮影:細谷亮介

GeNOM(ゲノム)とは

DeNAのゲームクリエイターを様々な切り口で紹介するメディア(運営:株式会社ディー・エヌ・エー)です。ゲーム開発の現場で生まれる様々なエピソードや、クリエイター紹介、イベント紹介などを通して、DeNAで働くメンバーの”ありのまま”をお伝えしていきます。

GeNOMの最新情報は、公式Twitterアカウントにて確認いただけます。ぜひフォローをお願いします!

JOIN US!
DeNAゲーム事業部の採用情報はこちら