iOS UIテストにて、アプリで保存したファイルの内容をテストコード側で検証したいことがありました。
結論からいうと、シミュレーターではできたけど、実機ではできませんでした。
できなかったけど色々試行錯誤したので、記録として残しておこうと思います。
やりたかったこと
例えば、画面内のあるボタンをタップすると、オンライン上のデータがテキストファイルとしてアプリ内に保存され、以降はオフラインでそのデータを閲覧できる機能を考えてみましょう。
UIテストでは、以下のようなテストコードを書くことになります。
- アプリを起動し、該当のボタンをタップし、ファイルのダウンロード完了まで待つ(UIテストのときは、そのファイルは共有ディレクトリに保存されるようにする)
- 共有ディレクトリに保存されたファイルをロードし、テキストの内容を検証する
iOSのUIテストでは、本体アプリと、テストを実行するテストランナーアプリがインストールされ、テストランナーアプリが本体アプリを操作するという仕組みを取っています。
iOSではSandboxという仕組みにより、基本的にアプリ間でのファイル共有はできないようになっています。
ただし、こちらの記事によれば、シミュレーターではシミュレーター内の共有ディレクトリ、実機ではApp Groupを使うことで、本体アプリとテストランナーアプリ間でのファイル共有を実現したとあります。
この記事のやり方で試したところ、シミュレーターではファイル共有できたものの、App Groupを使った実機でのファイル共有はできませんでした。記事が投稿されたのが2017年と古いので、もしかしたらiOSのセキュリティ周りがより厳しくなったのかもしれません。
シミュレーターでファイル共有
共有ディレクトリに保存されたファイルを検証するテストコードの例を書いてみました。
import XCTest class BambooCIAppUITests: XCTestCase { var sharedDirectoryPath: String { let simulatorSharedDirectory = ProcessInfo().environment["SIMULATOR_SHARED_RESOURCES_DIRECTORY"]! return (simulatorSharedDirectory as NSString).appendingPathComponent("Library/Caches") } func testExample() throws { let app = XCUIApplication() app.launchEnvironment["sharedDirectoryPath"] = sharedDirectoryPath app.launch() let path = "\(sharedDirectoryPath)/result.txt" let url = URL(fileURLWithPath: path) let data = try! Data(contentsOf: url) let resultText = String(data: data, encoding: .utf8) XCTAssertTrue(resultText == "Hello, World!") } }
SIMULATOR_SHARED_RESOURCES_DIRECTORY
というのはシミュレーターを起動すると自動で設定される環境変数で、シミュレーター内のアプリで共通アクセスが可能なディレクトリのパスが設定されています。
具体的には~/Library/Developer/CoreSimulator/Devices/<シミュレーターUDID>/data
というパスです。今回はこのディレクトリ内のLibrary/Caches
を共有ディレクトリとして扱います。
シミュレーターのフォルダ構成についてはこちらの記事で詳しく説明しています。
app.launchEnvironment["sharedDirectoryPath"] = sharedDirectoryPath
は、本体アプリに環境変数を渡すコードです。ファイルの保存先をテストコード側から伝えています。
続いて本体アプリ側を見ていきます。簡単のために、アプリを起動したらファイルが保存されるようにします。
import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let sharedDirectoryPath = ProcessInfo.processInfo.environment["sharedDirectoryPath"]! let filePath = "\(sharedDirectoryPath)/result.txt" let data = "Hello, World!".data(using: .utf8) FileManager.default.createFile(atPath: filePath, contents: data, attributes: [:]) } }
最後に、テストコードの以下の部分でファイル内容の検証を行っています。ここではファイルの中身がHello, World!
かどうかを検証しています。
let path = "\(sharedDirectoryPath)/result.txt" let url = URL(fileURLWithPath: path) let data = try! Data(contentsOf: url) let resultText = String(data: data, encoding: .utf8) XCTAssertTrue(resultText == "Hello, World!")
共有ディレクトリのパスをFinderで確認すると、たしかにresult.txt
というファイルが保存されています。
シミュレーターでのファイル共有はあっさり実現できました。
実機でファイル共有
実機でアプリ間でファイル共有をする方法として、App Groupという仕組みが用意されています。App Groupの設定を行うと、同一のApp Groupに所属するアプリだけがアクセス可能な専用のディレクトリが作成されます。
まずはApp Groupの設定をしていきましょう。
Apple Developerにログインし、App Groupを作成します。
続いて、XcodeでメインターゲットのApp Groupを設定します。
この状態で、テストコードの以下の部分だけ書き換えてUIテストを実行してみます。appGroupUrl
はApp Group用の共有ディレクトリのパスです。
class BambooCIAppUITests: XCTestCase { var sharedDirectoryPath: String { // let simulatorSharedDirectory = ProcessInfo().environment["SIMULATOR_SHARED_RESOURCES_DIRECTORY"]! // return (simulatorSharedDirectory as NSString).appendingPathComponent("Library/Caches") let appGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.bamboo-hero.BambooCIApp")! return (appGroupUrl.path as NSString).appendingPathComponent("Library/Caches") } ...
UIテストを実機で実行すると、以下のエラーが出力されてクラッシュします。
2021-05-14 02:10:37.708070+0900 BambooCIAppUITests-Runner[44865:3839805] [unspecified] container_create_or_lookup_app_group_path_by_app_group_identifier: client is not entitled BambooCIAppUITests/BambooCIAppUITests.swift:8: Fatal error: Unexpectedly found nil while unwrapping an Optional value
client is not entitled
、つまり、テストランナーアプリ側にApp Groupの設定がされていないと言われています。
では言われた通りApp Groupの設定をしてみましょう。具体的には、UIテストターゲットに設定していきます。
UIテストターゲットの場合、XcodeのSigning & CapabilitiesタブでApp Groupの設定をすることはできません。なので手動で無理やり設定してみます。
まず、UIテストターゲット用のentitlementsファイルを作成します。
次に、UIテストターゲットのBuild Settingsタブで、CODE_SIGN_ENTITLEMENTS
にentitlementsファイルのパスを指定します。
この状態でSigning & Capabilitiesタブを見ると、何やらエラーが出ています。(何をどう調べたか忘れたのですが)XcodeがUIテストターゲットのApp IDを登録するのに失敗しているようです。
ではApple Developerにログインして、手動でApp IDを登録しましょう。今回の例ではUIテストターゲットのバンドルIDはcom.bamboo-hero.BambooCIAppUITests
なので、この名前でApp IDを作ります。
すると...こんなエラーが...
xxUITests
という名前は予約されてるんでしょうか?仕方ないので、com.bamboo-hero.BambooCIAppGreatTests
という名前に変えて作成します。このApp IDにApp Groupを紐付けます。
Xcodeに戻り、UIテストターゲットのバンドルIDを変更します。Build Settingsタブで変更することができます。
これで署名周りのエラーがなくなりました。
ではテストを実行してみましょう!
...ダメでした。
テストランナーアプリのバンドルIDは最終的にcom.bamboo-hero.BambooCIAppGreatTests.xctrunner
になるみたいで、この名前のApp IDがApple Developerに登録されていないため、ワイルドカード指定のProvisioning profileが使用されるのですが、ワイルドカード指定のProvisioning profileではApp Groupが使用できないのです。
この先はもう省略しますが、com.bamboo-hero.BambooCIAppGreatTests.xctrunner
でApp IDを登録して、XcodeでバンドルIDを変更して、というのも試しましたが、結局どれもうまくいきませんでした。
実機でのファイル共有は諦めました。
まとめ
結局シミュレーターでしかできませんでしたが、アプリとテストランナーでファイル共有する方法について説明しました。
あれこれ試行錯誤して結局実機では実現できなかったのですが、おかげでiOSの署名周りの知識が結構身についた気がします。これはこれで良かったのかなと、今では思っています。