Unity で UI テストの自動化をやってみた

[PONOS Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ponos) の 14 日目の記事です。

隙間時間でじんわり実装していたら、同じアプローチをされている良記事が公開されました。
[Unityアプリを無差別タップで自動テストする](https://techblog.kayac.com/unity-automated-random-tap-test)
とてもきちんとまとめられているので、こちらを参照することをおすすめします。

あとは余談です。

- - -

# 動機

Unity で開発運用中のスマホゲームアプリで、UI テストの自動化をやりたい場面が増えてきました。
その場面というのは

- 特定の箇所(画面)をデバッグしたいので、起動したらそこまで自動で進んで欲しい[^select_scene]。<br />その間他のことができます。
- 画面をランダムタップ(いわゆるモンキーテスト)してほしい。<br />これは長時間の稼働に耐えるかどうかのチェック(エージング)と何かバグが隠れてないかあぶり出すためです。

まぁあるあるだと思います。特別難しい話でもありません。

[^select_scene]: 通常は「特定の画面」を呼び出すデバッグ用の機能を作るとは思いますが、今回の案件では対応しにくいので妥協しました。

# 要件

要件は次のようにしました。

- 端末で実行したい。<br />これは最終成果物で確認するためという理由によります。Unity Editor 上でも使えればなおいいです。
- 端末単体で実行したい。<br />空いてる端末さえあればテストできます。PC にいちいち繋がないといけないのはいろいろ面倒です。
- 押して欲しくないボタンを押さないようにしたい。<br />チェックしなくていい機能や押すとクラッシュするのがわかっているボタンを押しても余計に時間がかかるだけです。
- Android と iOS で同じ環境でテストできるようにしたい。<br />違う環境(ツール)が必要だと、メンテや利用者の学習の手間が増えてしまいます。

そのため、端末内で閉じているのは必須としました。

# 検討

いろいろ調べてみて、テストに使えそうな方法を挙げてみました。

|方法|概要|
|:-:|:--|
|[appium](http://appium.io/)|素の appium だと、例えば OpenCV と組み合わせるなど工夫が必要。|
|[AltUnityTester](https://altom.com/testing-tools/altunitytester/)|Unity に対応する Selenium driver を用意して appium を使う。|
|[GAutomator](https://github.com/Tencent/GAutomator)|ざっくりした理解だと AltUnityTester と同じ構成。|
|[adb shell monkey](https://developer.android.com/studio/test/monkey)|adb さえ使えれば何とかなるので環境構築が楽。|
|[SikuliX](http://sikulix.com/)|Android であれば Visor などを使えばとりあえず環境は作れそう。|
|[Airtest IDE](https://airtest.netease.com/)|ざっくりした理解だと AltUnityTester と同じ構成。|

どれも「端末内で閉じる」という要件を満たしません。

# 実装

そんなときに、Unity の EventSystem に対してコードから Event を発火できるという記事を目にしました(URL 失念)。

```c#
var g = gameObject; // 任意のゲームオブジェクト
var point = new PointerEventData (EventSystem.current);
point.position = new Vector2 (g.transform.position.x, g.transform.position.y);
ExecuteEvents.Execute<IPointerClickHandler> (g, point, (handler, eventData) => handler.OnPointerClick ((PointerEventData) eventData));
```

「これで Unity に閉じた UI タップ自動化ができる → 端末単体で動く」と早速実装に入りました。

そもそもタップした座標にタップできる GameObject があるかどうかをどうやって調べるかですが、以下のコードで可能です。

```c#
PointerEventData pointerEventData = new PointerEventData (EventSystem.current);
pointerEventData.position = new Vector2(0f, 0f); // 任意の座標
List<RaycastResult> raycastResult = new List<RaycastResult> ();
EventSystem.current.RaycastAll (pointerEventData, raycastResult);
```

これで任意の座標にあるタップできる GameObject が List で返ってきます。
任意の座標に、タップできる GameObject が複数ある場合に raycastResult の中身がどうなるかが問題だったんですが、動作検証していると「どうやら raycastResult[0] が最上位(= タップされるべき) GameObject」だということがわかったので、とりあえず raycastResult[0] を採用しています。このあたりは EventSystem のコードを読む必要があるのですが、今のところ誤動作らしき挙動は確認できていないので大丈夫そうです[^get_gameobjects]。
[^get_gameobjects]: GameObject が active でないとか interactable でないとか透明でタップされないはずなどの状況でどうなるかは未調査です。

# 結果

動機であった、特定の箇所まで進めるのと、押したくないものを押さないモンキーテストは実現できました。
見つけにくかったバグがこれで見つかったりしているので、やってよかったと思います。

## デメリット

いいことばかりではなく、課題も見えてきました。

- Unity の EventSystem での実装なので、EventSystem の制御が及ばない要素の自動化はできない。<br />例えば、OS ネイティブであるアプリ内課金のダイアログのボタンはタップできません。
- 特定の箇所に進めるために「どのボタンを押していくか」をコーディングする必要がある。<br />シーンの切り替えを待ったり、どのボタンを押すかなどいちいちコーディングする必要があるのは結構めんどくさいです。特にどのボタンを押すのかは GameObject の名前を調べる必要があり、これがかなり時間がかかります。この対応として、手でタップした GameObject を記録してあとで自動再生するようにするなど工夫がないと辛いです。

他にも、バッテリー消費がヤバいのもデメリットです。これはどちらかというと実装の問題ですね。

# 今後

対象にしているゲームアプリでは、タップ以外の操作(スワイプやピンチイン/アウトなど)は重要でなかったため対応していません。
また、タップはシングルタップです。もともと `Input.multiTouchEnabled = false;` なのでマルチタップのテストの必要性がなかったために手抜きしました。
必要性を感じているのは「同じボタンの連打」です[^taptaptap]。特にゲームはボタンを連打することが多い[^frastration]と思われるので誤動作しないようにチェックしたいのですが、これは人力より機械の方が向いています。
[^taptaptap]: 「同時押し」「連打」は対応が必須です。"OK" と "CANCEL" ボタンが同時に押せてそれぞれ処理が走るとまずいですし、連打することで同じ API アクセスが連続して起きるのもまずいです。ちゃんと作っていればこういうことは起きないはずですが、そうもいかない現場もありますし。このあたりの「仕様に現れないけど対応しておくべき事柄」をネタにしたほうが面白かったかも…
[^frastration]: 「早くローディング終わらんかなー」と画面を連打することってありますよね。


# おまけ

エンジニア的に楽しいはこういうやつですよね。

[第三回ソレコン応募作品「Sole」](https://www.youtube.com/watch?v=KP3BxubiECw)
[スマホ自動化！物理的に自動でスマホ・タブレット操作をするロボットの作成](https://www.tatetate55.com/entry/2016/11/01/232349)
[日本ノーベル株式会社 携帯端末評価支援 ロボットによる自動試験（Quality Commander）](https://www.jnovel.co.jp/service/mc-test/autotest-robot.html)

ポノスには「ハードウェアハック部」という部活動があるので、そこで作ってみるにはいいネタです。

気になっているのは、Android では「アプリから adb を叩く」方法が取れるらしく、これもいろいろ使えそうです。
[adb shellのコマンドをアプリ内から実行する](https://qiita.com/946wfrr/items/3f849f08e6f5b8cc0bf5)

明日は @honeniq さんです。