🌐

FastlaneのSnapshotのテストケース内でローカライズされた文字を利用したマッチをする

2021/09/19に公開

FastlaneのSnapshotについて

FastlaneSnapshot を利用すると、XCUITest を用いて iOS デバイスを操作し、自動で App Store 向けのスクリーンショットを撮影することができます。

App Storeのスクリーンショットは「スクリーンショットの枚数」「画面サイズ」「言語」を掛け算した枚数だけ用意する必要があります。

2021年9月18日現在、iPhoneとiPadの両方でApp Storeにアプリを提出するにはiPhoneが2種類、iPadが2種類の4サイズの端末で撮影されたスクリーンショットが必要です。2つ以上の言語に対応している場合、最低でも8枚以上の画像を用意する必要があり、手動で撮影するのは非常に大変です。

Snapshotを利用すると、XCUTestを用いて記述したUIテストに基づいてiOSシミュレーター上で自動でアプリを操作し、指定した名前でスクリーンショットを撮影できます。Deliver と組み合わせることでアップロードも自動で行うことができ、アプリリリース時の作業を大幅に削減することができます。

UIテストにおける要素のマッチング

XCUITestでアプリを操作する場合、用意されているクエリでボタンなど操作したい要素を探し、タップなど希望する操作をすることが基本となります。

要素のマッチには、大きく分けて Accesibility Identifierを用いる方法テキストを用いる方法 の2つがあります。

Accesibility Identifierを用いる方法では、UIを構築する段階で設定したAccesibility Identifierで要素を探すことができます。この方法は、信頼性の高いテストを記述することができる一方、全てのViewにAccesibility Identifierを設定するのは大変な他、ライブラリなどで作られたViewに対しては適用が難しかったりします。

そこで用いるのがテキストを用いたマッチングです。例えば、

Button(action: {
    // 押された場合の処理
}) {
    Text("次へ")
}

のようなViewがあったときに、テストコードで

app.buttons["次へ"].firstMatch.tap()

と書くことで、最初にマッチした 次へ と書かれたボタンをタップすることができます。

この他にも、要素のindex (2つ目のボタン など)で要素をマッチする方法もありますが、Viewを編集した際に簡単に壊れるため、Accesibility Identifierやテキストを用いてマッチすることが望ましいと考えています。

テキストを用いたマッチングと多言語対応

この方法を用いる際に問題となるのが、アプリのスクリーンショットを複数の言語で撮影したいときです。上記のコードのように、 次へ とテストコードにハードコードしてしまった場合、当然ですがアプリを英語で起動してボタンが Continue となった場合にはボタンをマッチすることができません。

ここで必要となるのがロケールとキーから適切な文字列を返す処理です。

XCUIApplicationのlaunchArgumentsからロケールを取得

iOSデバイスでロケールを取得する方法として一般的なのが、LocaleのIdentifierを Locale.preferredLanguages.first! で取得し、それを Locale(identifier:) を用いてLocaleにする方法です。

let locale = Locale(identifier: Locale.preferredLanguages.first!)

しかし、Snapshotではこのコードは動作しません。Locale.preferredLanguages はiOSの言語設定の値ですが、SnapshotではiOSシミュレータの言語は変更することなく、 launchArguments を利用することでテスト対象のアプリだけ言語を変更しています。

そこで、XCUIApplicationlaunchArguments で指定された -AppleLocale から値を取得することで、Snapshotで利用されているLocaleを取得することができます。

private func getLocale(app: XCUIApplication) -> String {
  guard let localeArgIdx = app.launchArguments.firstIndex(of: "-AppleLocale") else {
    return "en-US" // -AppleLocale が指定されない場合はデフォルトとして英語を使用
  }
  if localeArgIdx >= app.launchArguments.count {
    return "en-US"
  }
  let str = app.launchArguments[localeArgIdx + 1]
  let start = str.index(str.startIndex, offsetBy: 1)
  let end = str.index(start, offsetBy: 2)
  let range = start..<end
  let locale = str[range]
  return String(locale)
}

上記のメソッドをXCTestCaseに追加することで、XCUIApplicationインスタンスを渡すことでロケールを取得できます。 launchArguments での指定がない場合には英語を利用します。必要に応じて、デバイスの言語を返すようにしてもいいと思います。

ロケールとキーから翻訳テキストを取得

最後に、先ほど取得したロケールとテキストの翻訳キーを使って翻訳されたテキストを返却する関数を定義します。

func testExample() throws {
  let app = XCUIApplication()
  setupSnapshot(app)
  app.launch()

  // キーから翻訳済みテキストを返却する
  func localized(_ key: String) -> String {
    let locale = getLocale(app: app)
    let testBundle = Bundle(for: Self.self)
    // ロケールのバンドルを取得
    if let testBundlePath = testBundle.path(forResource: locale, ofType: "lproj"),
      let localizedBundle = Bundle(path: testBundlePath) {
      return NSLocalizedString(key, bundle: localizedBundle, comment: "")
    }
    return "?"
  }
}

最後に、localized 関数を使ってマッチします。

app.buttons[localized("CONTINUE")].tap() // `CONTINUE` キーに対応する現在の言語のテキストでマッチング

参考までに、 setupSnapshot(app) の前に app.launchArguments-AppleLocale {ロケール} を指定することで、Snapshotの外でテストを実行する場合のアプリの言語を変更することができます。UIテストにおいてデバイスの言語設定は使わずに launchArguments を用いたロケールの指定に統一すると、管理やデバッグがシンプルになるかなと思います。

最後に

少しトリックが必要な、Fastlane Snapshotで立ち上げるUIテスト内で翻訳テキストを用いて要素をマッチングする方法についてご紹介しました。

私が取り組んだときには、iOSのロケールの仕様に詳しくなかったため少し手間取りましたが、この方法で日本語と英語の両方に対応したアプリのスクリーンショットを素早く撮影することができました。似たところでハマった人の参考になれば幸いです。

Fastlaneの他プロダクトと同様にSnapshotのセットアップも簡単ではないですが、アプリを継続的にメンテナンスすることを考えると早めに整備できるといいと思います。

Discussion