Unity製プロジェクトにおいて、(ユニットテストでなく)UIを操作して動作を確認するテストを書くことがあります。
uGUIコンポーネントの操作に関しては『Unityバイブル R5夏号』の SECTION 06「ゲームプレイの自動テスト」の中で「アウトゲームのテスト」として紹介していますが、操作する GameObject
の検索に汎用的に利用できるAPIをMonkey Test Helperパッケージ(com.nowsprinting.test-helper.monkey)*1に追加しましたので紹介します。
UIテストの目的
UIを操作するテストは、なんらかのミスでSceneファイルを壊してしまったり、Sceneに追加したアセットがアセットバンドルに存在しなかったり*2といったリグレッションを検知する目的で行われます。 多くの自動テストがそうであるように、新規のバグを見つけることはあまり期待できません。
見た目の問題を検出するのは難しいのですが、リグレッション目的であれば Graphics Tests Frameworkパッケージ(com.unity.testframework.graphics)*3を使用するなどしてスクリーンショットを比較して合否判定する手法もあります。
Graphics Tests Frameworkパッケージによるビジュアルリグレッションテストについては『Unity Test Framework完全攻略ガイド 統合テスト編』で紹介していますので参考にしてください。
Monkey Test Helperパッケージ
MITライセンスで公開しているOSSで、主用途はuGUIのモンキーテストを行なうリファレンス実装です。 モンキーテストについては、前掲の『Unityバイブル R5夏号』および『Unity Test Framework完全攻略ガイド 統合テスト編』で紹介していますので参考にしてください。
パッケージのインストールは、openupm-cli を使用して次のコマンドで行なうのが簡単です。
openupm add com.nowsprinting.test-helper.monkey
Package Managerウィンドウによるインストール方法は、リポジトリのREADMEを参照してください。
インストールしたら、Play Modeテストアセンブリ(.asmdef)のAssembly Definition Referencesに TestHelper.Monkey
を追加します。
GameObjectFinder
Monkey Test Helperパッケージ v0.8.0で GameObjectFinder
クラスを追加しました。
Sceneに存在する GameObject
の検索は UnityEngine.GameObject.Find(string)
メソッドでも行えますが、この GameObjectFinder
には次の機能があります。
- 非同期で
GameObject
の出現を指定秒だけ待つ - レイキャストが通る(==隠されていない)オブジェクトのみを対象とするオプション
- 操作可能なコンポーネントを持つオブジェクトのみを対象とするオプション
GameObject
のパス(Glob可)を指定して検索するメソッド
レイキャストや操作可能判定についてはストラテジパターンで判定関数を差し替え可能にしてあり、uGUIに準拠していない独自UIフレームワークにも適用可能です。
GameObjectFinder
のコンストラクタには、次のように各asyncメソッドのタイムアウト時間を渡せます。デフォルトは1秒です。
private readonly GameObjectFinder _finder = new GameObjectFinder(3.0d);
独自UIフレームワーク向けの判定関数もコンストラクタ引数として渡します(本稿では詳細は割愛します)。
FindByNameAsync(名前で検索)
Sceneをロードして "Title" という名前のオブジェクト(CanvasでもPanelでも空のGameObject
でも)が表示されるのを待ち、さらに "StartButton" という名前の Button
を探してクリック、"Home" という名前のオブジェクトが表示されることを検証するテストは次のように書けます。
[Test] [LoadScene("Assets/Path/To/Scene.unity")] public async Task ScreenTransitionTest() { // "Title" という名前のオブジェクトの出現を待つ。reachableのデフォルトはtrue await _finder.FindByNameAsync("Title"); // "StartButton" という名前の操作可能オブジェクトの出現を待ち、クリック var startButton = await _finder.FindByNameAsync("StartButton", interactable: true); var startComponent = InteractiveComponent.CreateInteractableComponent(startButton.GetComponent<Button>()); Assume.That(startComponent.CanClick(), Is.True); startComponent.Click(); // "Home" という名前のオブジェクトの出現を待つ await _finder.FindByNameAsync("Home"); }
GameObject
の検索は、FindByNameAsync
で行ないます。
引数として GameObject
の名前のほか、レイキャストが通るオブジェクトのみを対象とするか、操作可能なコンポーネントを持つオブジェクトのみを対象とするかを指定できます。
デフォルトは「レイキャストが通る・操作可否は問わない」です。
コンストラクタで指定したタイムアウト時間までに GameObject
が見つからなかった場合、TimeoutException
が投げられます。
例外が投げられるとその時点でテストは失敗*4します。
そのため、このテストコードではアサーションを書いていません。
発見したオブジェクトを操作するのに InteractiveComponent
を使用していますが、これはクリックなどの操作をラップしているだけです*5。説明は割愛します。
FindByPathAsync(パスで検索)
名前の代わりにScene内のヒエラルキー階層を /
区切りにしたパス文字列で検索できます。
パスにはGlobパターンを指定できますが、名前部分には含めることができません。
たとえば、"Help" という名前の GameObject
の下にある "BackButton" という名前の GameObject
を検索したい場合、次のように書くことができます。
var backButton = await _finder.FindByPathAsync($"**/Help/**/BackButton", interactable: true);
このように、名前だけでは特定できない GameObject
が存在する場合でもピンポイントで検索できます。
またGlobパターンを使用できることで、ある程度はSceneの構造が変えられてもテストが壊れることを回避できるはずです。
テストシナリオ作成にあたっての注意
GameObjectFinder
を使うことで比較的簡単にUIを操作するシナリオテストを書けますが、シナリオテストは実行時間もかかり、ゲーム本体の変更によって壊れやすいテストです。
最低限の量にとどめ、特に、長い操作シナリオを多量に作ることは避けるのが賢明です。
たとえば画面遷移に着目したテストであれば、状態遷移は0スイッチカバレッジで十分なはずです。 『Unity Test Framework完全攻略ガイド』でも紹介しているテスト技法も参考に、無理なくメンテナンスできるテストを書きましょう。