Unity製Android/iOSアプリのGUIを自動テストする仕組みを作ってみた

# 簡単にまとめると
unityで複数プラットフォーム開発をしている！
その中にmobileアプリもあって、Android/iOS共通のコードで動いている！
みたいな状態で、モバイルアプリの自動テストを導入してみたよって話です
メモ書き程度に残そうと思い書きました。

# プロダクトについて
自分がかかわっているSTYLYというXRプラットフォームはUnityで作られています。
クロスプラットフォームでモバイルのほかにWebや多数のHMDに対応しています。
https://gallery.styly.cc/about/ja
これらのことを踏まえてお読みください

# 事の始まり
世の中では、モバイルアプリの自動テストができるツールが最近多く出てきましたね
* Appium
* Magic Pod
* Autify for Mobile
    * などなど

これらの自動テストツールは大きくふたつのUI操作方法があります
1. UI要素を指定して操作する
1. 画像検知で操作する

両方ともいい場面もありますが、欠点を抱えています
### UI要素を指定して操作する場合の欠点
#### 欠点1:ネイティブで書かれたアプリじゃないと操作できない
Unityで作られたアプリはUI要素上、一つの画面があるだけに扱われ、操作ができませんでした

#### 欠点2:仮にネイティブで書かれていたとして、Android/iOSでテストコードが変わるので保守性が悪い
そもそもandoridとiOSでネイティブの言語が違うためそれに対応したテストコードを二つ書くことになります
そもそもクロスプラットフォームでモバイルなんて環境のうちの1個にしか過ぎない弊社プロダクトでは
そこまでの時間も作れません

### 画像検知で操作する場合の欠点
#### 欠点1:画像検知なので、同じ画像が使いまわせない
Andorid/iOS間で色味が微妙に違うこと、多々あります
そんな時にiOS用の画像、Android用の画像をいちいち取るのは二度手間で保守性が低いと感じます

#### 欠点2:なぜか冪等性が保たれないことがある
何回か繰り返しているとなぜが特定の画像で転ぶ、
リトライすると大丈夫。みたいな現象がSikuliXを使ったWebのGUIテストをしていた時に頻発していました
そのたびに画像を取り直したりと対応に追われていたので画像検知はあまり信頼していませんでした

そこで！
Unityでも操作できて。かつクロスプラットフォームで同じテストコードが使いまわせるものは無いかと探していた時に出会ったのが **AirTest + Poco**でした

 **Airtest**はNetEase社が提供しているテストフレームワークです。
Pythonで動きIDEのほかにCLIも存在します

 **Poco**はUnityのプロジェクトにPocoInspectorというデバッグスクリプトを置いて置くと、ヒエラルキー上にあるObjectの名前を使って、その名前Objectの座標を取得し、操作することができる機能を備えたテストフレームワークです

詳しくはこちら↓の記事を参考にしてください
[Airtest+Pocoで始めるUnityの自動テスト](https://qiita.com/Nitudon/items/0c5c527841388fec22ed)

これらを使ってUnityの自動テスト環境を構築しよう！！というのが本記事の内容です。

# 何をしたのか
Airtest + pocoをgithub Actionsで動かします
ちなみに弊社ではDevelopビルドはUnity Cloud Buildで動かしています。

GithubでPRが作成されるたびに、Unity Cloud Buildで、PR確認用のビルド作成が走る感じです。
そこで **リグレッションテストとして、PR作成後のビルドを自動でテストしよう！** と思ったわけです。

ただしここで、一つの障壁が生まれました
 **Github Actionsからスマホをどうやって繋げるのか？？** という問題です。

[STFを使うこと](https://techblog.istyle.co.jp/archives/6206)などを考えてみましたが
iOSの接続がうまくいかん！！！ということで
Macのself-hosted-runnerを用意し、各スマホたちをつなぐことにしました。

|  | Android | iOS |
|:-:|:-:|:-:|
| 接続先 | Mac | Mac |
| 接続方法 | adb | iproxy + WebDriverAgent |

### 構造図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/705249/f66f5db0-1d78-57a0-f6a7-3d2aab356186.png)

GithubActonsのymlファイルは **ビルド用yml** と **テスト用yml** の、大きく二つのファイルで動かしています。

### ビルド用yml
PRが開かれたら、Unity Cloud BuildにBuildTargetを登録しビルドをする。というymlです。
Github Actions上で動かしてます。

### テスト用yml
以下の挙動をします。
1. Unity Cloud Buildのビルドを待つ。
1. apkとipaファイルを取得する。
1. 各デバイスにアプリをインストール
1. 決められたテストケースをAirtestで実行する
1. エラーがある場合のみ、テストレポートを生成する。
 ※3,4はpythonで挙動を記述してます。

これらはすべて、Mac Bookのself-hosted-runnerで動かしています。

#### Unity Cloud Buildのビルドを待つ
以下のコマンドでcurlした結果(json)をjqでパースする形で
ビルド結果が取れるので、Successになるまで5分おきにポーリングしています。

``` shell:
curl -X GET https://build-api.cloud.unity3d.com/api/v1/orgs/<orgID>/projects/<projectID>/buildtargets/<buildTargetsName>/builds \
-H "Authorization: Basic <APITOKEN>" | jq -r '.[-1:]|.[].buildStatus'
```

#### apkとipaファイルを取得する
以下のコマンドでcurlで取得したダウンロードリンクを流用してダウンロードしてきます。
``` shell:
curl -X GET https://build-api.cloud.unity3d.com/api/v1/orgs/<orgID>/projects/<projectID>/buildtargets/<buildTargetsName>/builds \
-H 'Content-Type: application/json' \
-H 'Authorization: Basic <APITOKEN>'| jq '.[-1:]|.[].links.download_primary.href' | xargs curl -o <filename>'
```

#### 各デバイスにアプリをインストール
ここはpythonをself-hosted-runner上で実行しています。
``` shell:iOSのアプリインストール方法
ideviceinstaller -i <filename>
```
``` shell:Androidのアプリインストール方法
adb install -r <filename>
```
上記のコマンドをpythonでたたいた後

アプリを起動し
``` python:アプリ起動方法
start_app(packageName)
```
初回起動時のチュートリアルをAirtest + pocoでスキップしています。

#### 決められたテストケースをAirtestで実行する
airtestのpure Pythonのテストケースの書き方でテストを書いています。
テストを書いていると、GameObjectをちゃんと指定したのに押せない。みたいなことが結構あります。

その時は、UIのテキストで引っ掛けるとうまくいく場合があります！
``` python:pocoでUIのテキスト内容で引っ掛けて押下する
poco(text="OK").click()
```
これで”OK”と記述されたUIボタンを押すことができます。
便利！

ちなみにpythonコードの実行はAirtestIDEのCLIコマンドを使用しています。
``` shell:Airtest IDEをCLIで実行する
/Applications/AirtestIDE.app/Contents/MacOS/AirtestIDE runner <airtestのプロジェクト名>.air --device Android:/// --log <logFileName>
```

runnerの後に入れるairtestのプロジェクト名は
airtestIDEで作れるpythonで出来たプロジェクトです
テストケースがpythonで記述して.pyで保存し、airtestのプロジェクト側からimportして呼び出すようにしてあります。

#### エラーがある場合のみ、テストレポートを生成する。
airtest IDEのreporter機能を使います。
``` shell:Airtest IDEをCLIで実行する
/Applications/AirtestIDE.app/Contents/MacOS/AirtestIDE reporter <airtestのプロジェクト名>.air --log_root <logFileName> --export <reportFileName>
```
こんな感じで、htmlファイル群が生成され、どこで何を押そうとしたかが視覚的にわかるファイルが生成されます。


## まとめ
以上のような感じでAirtest + pocoをクラウドベースで実行し、結果を管理することで
PRごとにGUIテストを走らせる環境を作ってみました。

まだまだ問題は山積みですし、(Unity Cloud BuildをAPIをポーリングしていいのかとか)
整備しなきゃいけないことは多いですが、おおむね動いています。

self-hosted-runnerのMacはまだ自宅で稼働していて、オフィスに移植していないので
そろそろ移植したいなーの気持ちもあります。。。

質問、感想はじゃんじゃん投げてください！