Unity製iOSアプリの実機テストを手軽にクラウドシフトしよう

by shintaro-takemura | September 28, 2020
continuous-integration-and-delivery | #dena #gcp #ios #unity #firebase

要点

  • モバイル端末上のテストをクラウドシフトすることで、新旧様々な検証用端末を購入するコストが抑えられ、またOSのバージョン管理から解放される。
  • ただしC#で書かれるUnityは、既存のネイティブ言語用テストフレームワークとの互換性がないため、クラウド事業者が提供するマネージドサービスをそのまま使えるとは限らない。
  • その中でFirebase Test LabのGame Loop testならUnityを扱えることを確認した。ただしiOS上で実現した先行事例はなく、参考資料も見当たらないため、その導入手順をコード付きで解説する。
  • 組み込み対象となるUnityのバージョンは2018.1以降を想定しているが、テスト実行に必須としない処理では2019.3以降の機能にも言及している。

はじめに

はじめまして、AIシステム部MLエンジニアリンググループの 竹村(@stakemura) です。普段は、AIの技術開発に携わっており、今回はその重要な動作対象プラットフォームの1つであるiOSと、ニーズの多いUnity上の自動テストに焦点を当てたいと思います。所属部署名からは想像しにくい内容で恐縮ですが、AI技術をモバイルアプリに正しく組み込むことも重要な業務の1つです。

実機テストのクラウドシフト

さて、皆さんはAWSやGCP、そしてAzureといったクラウド事業者が、いわゆるサーバーだけではなくモバイル端末のテスト用にマネージドサービスを提供していることをご存知でしょうか?本稿で述べる端末とは、もちろんエミュレータではなく実機そのものを指しており、ハードウェア依存のテストにも耐え得ることを前提とします。

モバイルアプリ開発は、次から次へと増え続ける端末、そして仕様が変わり続けるOSに対応し続けなければいけないという、PC上の開発にはない難しさがあります。また、新機種だけなく、どこまで古い端末をサポート対象とするかの検証も製品開発には必要です。このように、新旧様々な検証用端末を物理的に揃えるのはコストがかかりますし、OSのバージョン管理だって大変です。一方、会社として共有の開発用端末をオフィスに確保しているケースもあると思いますが、このリモートワーク時代、端末のためだけに出社するのも億劫という方も少なくないのではないでしょうか?

そこで、クラウドシフトです。前述のクラウド事業者は、モバイル端末の自動テスト用に、下記のサービス(以下、クラウドサービスと呼ぶ)を提供しています。

クラウドサービスの選定

ここから本題なのですが、テストしたいアプリの実装によっては「どのクラウドサービスを選定しても機能上問題ない」とは限りません。例えば、AWS Device Farmの対応フレームワークを該当ドキュメントから列挙してみましょう。

  • Appium
  • Calabash
  • Instrumentation (JUnit, Espresso, Robotium …) for Android
  • UI Automator for Android
  • UI Automation for iOS
  • XCTest
  • XCTest UI

お気づきでしょうか?Unityの名前がないことに。前述のどのクラウドサービスも、EspressoXCTestといったネイティブ言語で利用することを前提としたテストフレームワークには対応しています。ところが、非ネイティブ言語であるC#を採用するUnityは、それらテストフレームワークとの互換性がないため、クラウドサービスとしてサポートされるとは限らないのです。もちろん、アプリの起動ならできるでしょうが、それだけだと別途テスト結果の可視化などを自前でサポートする必要が出てしまいます。一方で、AWS Device Farmの機能を有効活用するために、前述のテストフレームワーク、例えばiOSであればXCTestに適合できるように、Unity as a Library を使ってネイティブ言語でUnityモジュールの呼び出しを頑張るアプローチが考えられます。しかし、このアプローチではどうしてもiOSとAndroidで実装が大きく別れますし、C#のみでクロスプラットフォームアプリが書けるというUnityの手軽さがスポイルされてしまいます。

Unity製iOSアプリにおける選択肢

本稿で推奨するアプローチは、ずばりGoogleの Firebase Test Lab です。このクラウドサービスには、Game Loop test(iOS / Android)という、その名の通りゲームにおける利用を前提としたモードがあり、実はAndroid版Unityに関しては、GoogleのCodelabsにて “Testing a Unity Project with Firebase Test Lab for Android” という名のドキュメントが公開されています。またドキュメントで参照しているunitypackageのコードは、googlecodelabs/unity-firebase-test-lab-game-loop で公開されています。ですので、Android版Unityに関しては、Google公式のドキュメントやレポジトリが頼りになります。

一方、問題はiOSです。本稿執筆時点で、GoogleはiOS版Unityには言及しておらず、また特に他社の先行事例も見当たりませんでした。したがって、まだ誰も挑戦したことがない試みである可能性が高いのですが、Firebase Test LabはiOS版Unityも問題なく扱えることを当方で確認済みです。ただそのためにはC#ベースの追加実装が必要であり、皆さんが手軽に再現できるよう、これからその実装内容をコード付きで解説いたします。

なお補足になりますが、Unity同様にC#を実装言語とする Xamarin のテストであれば、App Center を使うことを推奨します。こちらは、App Center公式ドキュメントの Get Started with Xamarin にて、AndroidとiOSの双方にSDKとして対応していることが示されています。

導入手順

Firebase projectの作成

Firebase Test Labを利用するためには、まずFirebase projectをご用意ください。Codelabsの 14. Creating a Firebase Project からの引用になりますが、作成手順は、以下の通りです。

  1. Firebase console を開く
  2. Add projectをクリック Creating a Firebase Project 2
  3. ダイアログでProject name を入力します。 Creating a Firebase Project 3-4
  4. 最後に、Create Projectをクリックしてください。

gcloud CLIのセットアップ

続いて、gcloud CLIがインストールされていなければ、インストールすることを推奨します。Firebase Test LabはWeb UIで扱うことができますが、テストのように繰り返し行うものは、コマンドで自動実行できるようにすることが望ましいためです。macOSにおけるgcloud CLIのインストールコマンドは下記の通りとなります。なお、本稿はGoogle Cloud SDK 309.0.0 を元に執筆しております。

$ curl https://sdk.cloud.google.com | bash

そして下記コマンドを実行して、Firebase projectを作るときに用いたGoogle Accountにログインしてください。なおCI用に、認証鍵のJSONファイルを使って認証する方法もあります。その場合は、gcloud auth activate-service-accountを参照してください。

$ gcloud init

次に、Firebase projectを作成すると、Project IDが自動的に生成されます。これは、Firebase consoleの各プロジェクトのpane上に表示されているもので、下記コマンドでテストに用いるプロジェクトを指定してください。

$ gcloud config set project ${GOOGLE_PROJECT_ID}

ここまで設定が終わったら、試しにFirebase test labがサポートするiOS端末やOSのバージョンを下記コマンドでチェックしてみましょう。途中割愛しておりますが、以下のような出力になるはずです。

$ gcloud firebase test ios models list
┌─────────────┬───────────────────────┬─────────────────────┬──────────────────────────────────┐
│   MODEL_ID  │          NAME         │    OS_VERSION_IDS   │               TAGS               │
├─────────────┼───────────────────────┼─────────────────────┼──────────────────────────────────┤
│ ipad5       │ iPad (5th generation)11.2,12.0           │ deprecated=11.2                  │

...

│ iphonexsmax │ iPhone XS Max         │ 12.0,12.1,12.3      │                                  │
└─────────────┴───────────────────────┴─────────────────────┴──────────────────────────────────┘
WARNING: Some devices are deprecated. Learn more at https://firebase.google.com/docs/test-lab/available-testing-devices#deprecated

余談ですが、Androidは下記コマンドで140種類以上の端末がリストアップされることが確認できます。

$ gcloud firebase test android models list

Firebase Test LabのUnity組み込み

ここからは、Firebase公式ドキュメントの Get started with Game Loop tests for iOSを参照しつつ解説いたします。なお、Unityのバージョンは2018.1以降を想定しています。

まずStep 1は次のように記されています。

Step 1: Register Test Lab’s custom URL scheme

  1. In Xcode, select a project target.
  2. Click the Info tab, then add a new URL type.
  3. In the URL Schemes field, enter firebase-game-loop. You can also register the custom URL scheme by adding it to your project’s Info.plist configuration file anywhere within the <dict> tag:

Firebase Test Lab上でテスト実行する上で、上記設定は必須です。Info.plistcustom URL scheme を加えるよう記載されていますね。これは IPostprocessBuildWithReport.OnPostprocessBuild にてビルド後にカスタム処理を加えることで実現できます。具体的には以下のようなコードになります。

#if UNITY_EDITOR
using System.IO;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;

#if UNITY_IOS
using UnityEditor.iOS.Xcode;
using UnityEditor.iOS.Xcode.Extensions;
#endif

namespace Firebase.TestLab
{
    public class BuildProcessor : IPostprocessBuildWithReport
    {
        public int callbackOrder { get { return 10; } }

        public void OnPostprocessBuild(UnityEditor.Build.Reporting.BuildReport report)
        {
#if UNITY_IOS
            // Custom URL scheme for Firebase Test Lab
            // https://firebase.google.com/docs/test-lab/game-loop

            var path = Path.Combine(report.summary.outputPath, "Info.plist");
            var doc = new PlistDocument();
            doc.ReadFromFile(path);
            var urlTypes = doc.root.CreateArray("CFBundleURLTypes");
            var dict = urlTypes.AddDict();
            dict.SetString("CFBundleURLName", "");
            dict.SetString("CFBundleTypeRole", "Editor");
            var urlSchemes = dict.CreateArray("CFBundleURLSchemes");
            urlSchemes.AddString("firebase-game-loop");
            doc.WriteToFile(path);
#endif // UNITY_IOS
        }
    }
}
#endif // UNITY_EDITOR

つぎに、Step 2の記載を確認します。

Step 2 (optional): Configure your app to run multiple loops

If your app has multiple custom URL schemes registered and you plan to run multiple loops (aka scenarios) in your test, you must specify which loops you want to run in your app at launch time.

optionalと記載されている通り、Step 2の対応はテスト実行に必須ではありません。そのため本稿では扱いませんが、もしStep 2を組み込んでテスト範囲を選択可能するのであれば、Unity公式ドキュメントのEnabling deep linking を参考に、Application.deepLinkActivated にカスタム実装を加えることになります。

そしてStep 3のテスト実行に入る前に、End a test early について解説します。これは実用上必須なもので、下記引用分にある通り、正しくテストが終了したことをFirebase Test Labに知らせるため custom URL schemefirebase-game-loop-complete 呼び出しが必要です。この呼び出しがないと、テスト実行は失敗(タイムアウト)と処理されてしまうのでご注意ください。 また Write custom test results 。これはログなどの出力を、後の参照のためにアップロードするもので、やはり実用上必須です。以下引用文にある通り、アプリ用のデータ保存パス内の GameLoopsResults フォルダ下にあるファイル群がアップロード対象となります。

以下は、上記の終了処理とログ出力をC#で実装したものです。 googlecodelabs/unity-firebase-test-lab-game-loop 内の Firebase.TestLab.TestLabManager を継承する形で実装されているため、Codelabsの 11. Fleshing out the Test Loops Script に解説されているように、iOSTestLabManager のインスタンスに対して、LogToResults()NotifyHarnessTestIsComplete() を適宜callしてください。

using System;
using System.IO;
using UnityEngine;

namespace Firebase.TestLab
{
    internal sealed class iOSTestLabManager : TestLabManager
    {
        public iOSTestLabManager()
        {
            string logPath = Path.Combine(Application.persistentDataPath, "GameLoopResults");
            if (!Directory.Exists(logPath))
            {
                Directory.CreateDirectory(logPath);
            }
            logFile = Path.Combine(logPath, "results_scenario_0.json");
        }

        public override void NotifyHarnessTestIsComplete()
        {
            Application.OpenURL("firebase-game-loop-complete://");
        }

        public override void LogToResults(string s)
        {
            StreamWriter writer = new StreamWriter(logFile, true);
            writer.WriteLine(s);
            writer.Flush();
            writer.Close();
        }

        private readonly string logFile;
    }
}

ここで、ログファイルをJSONフォーマットとしたのは、Android用の実装である Firebase.TestLab.AndroidTestLabManager の挙動に合わせるためです。Codelabsの 16. Checking the Results of a Test からの引用になりますが、Androidテスト完了時のスクリーンショットから、ログの拡張子は .json となっていることが確認できます。

  1. テスト端末を選択 Checking the Results of a Test 1
  2. Resultsに出力したJSONファイルが表示され、選択すると内容が表示される Checking the Results of a Test 2

iOS版もAndroid版同様にJSONフォーマットであることを前提にログ出力することを推奨します。理由として Firebase Test LabのWeb UIはJSONデータのツリー構造付き表示に対応しており視認性が良いこと、またJSONファイルはParseが容易であり、複数端末でのテスト結果を集計するといった機械的処理に向いているためです。幸い、UnityでのJSON出力は JSON Serialization で簡単に対応することができます。

Firebase Test Labのテスト実行

Step 3のテスト実行に戻ります。Firebase Test Labは、Webブラウザからipaファイルをアップロードし、端末をフォームから選択することでGUIベースのテスト実行が可能です。その際の具体的な操作手順は、Codelabsの 15. Running a Test が、参考になるでしょう。ですが、前述した様にテスト実行のような定型処理は、gcloud CLIでスクリプト化することを推奨します。

以下、テスト実行のコマンド例になります。

gcloud alpha firebase test ios run --app $BITRISE_IPA_PATH --type=game-loop --device model=iphone8,version=12.0,locale=ja_JP

引数の解説です。まず --app オプションで、XcodeでArchiveしたipaファイルのパスを指定し、--type オプションで、Game Loop testであることを指定しています。次に --device オプションで、テスト用端末のMODEL_ID、iOSのバージョン、ロケールをそれぞれ設定しています。MODEL_IDや対応するiOSのバージョンは、前述の gcloud firebase test ios models list コマンドで確認してください。

そして、本CLIのコマンドは、GCP公式ドキュメントの gcloud alpha firebase test ios run にて、以下の記載がある(※本稿執筆時点)通り、gcloud コマンドに続き alpha の指定が必要なことにご注意ください。

(ALPHA) gcloud alpha firebase test ios run invokes and monitors tests in Firebase Test Lab for iOS.

テスト実行コマンドの重要なオプションを2点紹介します。

  • --no-record-video

動画での記録をOFFにします。テスト実行の確認がテキストログで完結するものは、このオプションを指定した方がストレージの節約になるでしょう。

  • --results-bucket

テスト実行結果の出力先を、GCS(Google Cloud Storage)のバケット名で指定します。ログを機械的に処理する場合は、このオプションで出力先を固定しましょう。なおGCS上のファイルは、gsutilコマンドで取得可能です。

以下のスクリーンショットは、Unity製iOSアプリのFirebase Test Lab実行例になります。

iOS game loop test

iOS game loop test

まとめ

以上、Unity製iOSアプリでFirebase Test Labを使うための手順を解説いたしました。Firebase Test Lab自体は、Firebaseの料金プランによると無料のSpark プランでも、物理デバイスでのテストが最大5件/日実行できるとのことなので、是非試してみてはいかがでしょうか?他のクラウドサービスも同様だと思いますが、Firebase Test Labは本稿執筆時点で iPhone 11 ProPixel 4 といった、実売価格にして8万超のハイエンド端末を使うことができます。そのため、実機を代替することが端末購入前に実証できれば、コスト削減になるでしょう。

ところで本稿では書ききれなかったノウハウも残っています。実際のプロダクト開発では、Android向けに追加実装していますし、JSONベースのProfiling機能を付けたり、Cysharp/RuntimeUnitTestToolkit を活用して Play Mode test をFirebase Test Lab上で動かすといった工夫を行っています。これらについては、いつかまた別の機会に発表できたらと思います。

最後に、今後のアップデートはTwitter アカウント @DeNAxTech にて情報発信されると思いますので、こちらの記事が面白かったと思う方はぜひフォローをお願いします!