EarlGrey2を触ってみた

iOSのUIテストを支援するGoogle謹製ライブラリ[EarlGreyのバージョン2.0](https://github.com/google/EarlGrey/tree/earlgrey2)が2018年10月2日に公開されました。まだベータ段階ですが一通り動くようなので、概要やインストール方法などを解説します。

## EarlGrey1系について

[EarlGrey1系](https://github.com/google/EarlGrey)はざっくり言うとXCTest上で動作するUIテスト支援ライブラリです。（XCUITest上ではない）

なぜそんなものが必要なのか。
Xcodeの標準UIテストであるXCUITestはテスト対象のアプリを外側から操作するコードを書くことができるフレームワークです。
このためXCUITestで書いたテストコードは別アプリとして開発端末にインストールされます。
別アプリなので本体アプリ側のUserDefaultsの状態を変更するといった、テストの前提条件を整える処理をテストコードとして書くことができません。
できることはアプリのUIを介した操作だけです。
このことをEarlGreyでは、XCUITestはブラックボックステストだ、と表現しています。

これだとテストの自由度が低すぎますよね。
これを解消するためにEarlGrey1系では、本体アプリ側の処理ができるXCTest上でUI操作やUI要素のアサーションができるライブラリを提供していました。

イメージとしては以下の図のような関係性です。

![EarlGrey1.png](https://qiita-image-store.s3.amazonaws.com/0/36771/4735e9ee-423c-9bc5-fb36-e3230ec7bc40.png)

## EarlGrey2.0へ

素晴らしいEarlGrey1系ですが、いくつか解決が困難な課題を持っていました。
例えばiOSのシステムが表示するパーミッションダイアログを操作できなかったりします。
前もってパーミッションの操作をしておくとか運用で逃げることはできる（はず）ですが、あまりイケてないですよね。

そこでEarlGrey2.0では考え方をガラリと変えました。
XCTestで限界があるならば、XCUITestをホワイトボックス化すればいいじゃないか！と方針変更したのです。

ホワイトボックス化をどのように実現しているか。
HelperBundleというターゲットを別途用意し、これを通して本体アプリ側の処理をするアプローチを取っています。

関係性はこんな感じになります。

![EarlGrey2.png](https://qiita-image-store.s3.amazonaws.com/0/36771/3f024514-c6ee-a2c1-6a4c-c4d154ac2431.png)

## 導入手順

ベータのためCocoapodsやCarthageにはまだ対応していません。（次の四半期でやるよ！って書いてあります）
このためインストールは普通よりも手順が多いです。
またホワイトボックス化の重要な要素であるHelperBundleは個別に用意する必要があるため、パッケージ管理では対応できないんじゃないかなーと思ってたりします。
ほぼ[公式マニュアル](https://github.com/google/EarlGrey/blob/earlgrey2/docs/setup.md)の日本語訳ですが、誰かの助けになれば幸いです。

以下、例ではEarlGrey2を導入するプロジェクトの名前をWorkEarlGrey2として説明します。

#### [EarlGrey2のインストール](https://github.com/google/EarlGrey/blob/earlgrey2/docs/setup.md)

1. EarlGrey2をダウンロード
`git clone --recursive -b earlgrey2 https://github.com/google/EarlGrey.git` を実行。
`--recursive` をつけることで、HelperBundleを動かすキーとなる `eDistantObject` ライブラリが一緒に落ちてくるので、このままコピペすることをおすすめします。

1. EarlGrey2をXcodeプロジェクトに取り込み
EarlGreyを導入したいXcodeのプロジェクトを開いて、ワークスペースに `EarlGrey.xcodeproj` ファイルをドラッグ＆ドロップします。
必ずプロジェクト内にドロップしてください。外側に配置されてしまうと別のワークスペースになってしまいます。
<img width="217" alt="workspace.png" src="https://qiita-image-store.s3.amazonaws.com/0/36771/e75d8efb-146c-c072-a7bc-840636a9602c.png">


1. EarlGreyのライブラリがビルドできるか確認
うまく取り込めていればXcode左上のビルドターゲットにEarlGreyのライブラリが追加されています。
ここで一度ビルドターゲットを変更して、 `TestLib` と `AppFramework` がビルドできるか確認しておきます。

#### XCUITestの設定変更

XCUITestターゲットの設定を変更して、EarlGreyが動くようにします。

1. Build Phases - Link Binary with Libraries で `libTestLib.a` を追加
<img width="558" alt="testlib.png" src="https://qiita-image-store.s3.amazonaws.com/0/36771/1049c258-53fa-f80e-2602-48471064ed49.png">

2. Build Settings - Other Linker Flags に `-ObjC` を追加
<img width="1153" alt="linkerObjC.png" src="https://qiita-image-store.s3.amazonaws.com/0/36771/c9e9ab60-db70-141f-ab50-2a23099615d5.png">

3. Build Settings - User Header Search Paths に `EarlGrey.xcodeproj` と `eDistantObject.xcodeproj` があるディレクトリを追加
<img width="938" alt="userHeaderSearchPaths.png" src="https://qiita-image-store.s3.amazonaws.com/0/36771/86e839e7-77ae-d7f4-51d2-f5c943357509.png">
プロジェクトルートからの相対パスで記述できます。例ではプロジェクトルート直下にEarlGreyを落としてきています。

4. Build Phases に 新しい Copy Files Phase を追加
    1. 左上の＋ボタンを押して、 `New Copy Files Phase` を選択
    2. 追加した Copy Files Phase に以下を設定
        * Destination: `Absolute Path`
        * Path: `$(TARGET_BUILD_DIR)/../../<YOUR_APPLICATION_TARGET_NAME.app>/Frameworks`
        * `Copy only when installing` のチェックは外す
        * 下の方にある＋ボタンを押して `AppFramework.framework` をコピー対象に追加。 `Code Sign On Copy` にチェックをする。
<img width="1221" alt="copyAppFramework.png" src="https://qiita-image-store.s3.amazonaws.com/0/36771/4a328177-9567-0722-f3c3-c0f8dbb28a6d.png">

5. Swiftでの設定
Swiftプロジェクトの場合はいくつか追加の手順が必要になります。
    * EarlGrey.swift の追加  
SwiftソースからEarlGreyの機能を呼び出せるように、同梱されている[EarlGrey.swift](https://github.com/google/EarlGrey/blob/earlgrey2/TestLib/Swift/EarlGrey.swift)をXCUITestプロジェクト内に追加します。
    * Bridging-Header の設定  
EarlGreyの機能を呼び出せるように Bridging-Headerを追加します。
[EarlGreyで用意しているBridging-Headerファイル](https://github.com/google/EarlGrey/blob/earlgrey2/TestLib/Swift/EarlGreySwiftBridgingHeader.h)を使ってもいいですし、新しく用意してもいいです。（参考：[1分で出来る、SwiftにBridging Headerを追加しProjectに反映 - Qiita](https://qiita.com/flowtumn/items/fb97fd32d00deefad679)）
記述内容は[EarlGreyで用意しているBridging-Headerファイル](https://github.com/google/EarlGrey/blob/earlgrey2/TestLib/Swift/EarlGreySwiftBridgingHeader.h)を参照ください。
    * Runpath Search Path の設定  
XCUITestプロジェクトのBuild Settings にある `Runpath Search Paths` に `@loader_path/Frameworks` を追加します。

6. XCUITestをビルドして確認
ここまでがいったんXCUITestへのEarlGreyインストール手順となります。
EarlGrey1系で使っていたUI操作の処理などはこの時点で可能です。
テストコードがビルドできるか確認しておきましょう。

#### [HelperBundle の作成](https://github.com/google/EarlGrey/blob/earlgrey2/docs/white-boxing-setup-guide.md)

ここからがEarlGrey2.0の肝になるHelperBundleの作成です。

1. HelperBundleターゲットを追加
    1. Xcodeのメニューから File >> New >> Target を選択
    1. macOS のタブを選択
    1. Bundle を選んで Next <img width="731" alt="addHelperBundleTarget.png" src="https://qiita-image-store.s3.amazonaws.com/0/36771/e90633cb-9ace-2caa-8a93-959d01d29e10.png">
    1. 名前などは適当につける（例では EarlGreyHelperBundle とした）

1. HelperBundleターゲット の Build Settings を以下のように設定
    * Base SDK: `Latest iOS`
    * Other Linker Flags: `-ObjC` を追加
    * Bundle Loader: `$(TARGET_BUILD_DIR)/<YOUR_APPLICATION_TARGET_NAME.app>/<YOUR_APPLICATION_TARGET_NAME>` を設定 <img width="948" alt="helperbundlebundleloader.png" src="https://qiita-image-store.s3.amazonaws.com/0/36771/85d9b694-0e01-d75a-83e7-bbf1a9e49522.png">
    * User Header Search Paths: `EarlGrey.xcodeproj` と `eDistantObject.xcodeproj` があるディレクトリを追加 <img width="1216" alt="helperbundleheadersearchpaths.png" src="https://qiita-image-store.s3.amazonaws.com/0/36771/d4b0f6ec-3ccc-5705-bf93-4180bc919257.png">
    * （Swiftのみ）RunPath Search Paths: `@loader_path/Frameworks` を設定
    * Always Embed Swift Standard Libraries: `Yes`
        * ※公式マニュアルに記載がないが、サンプルプロジェクトで記載されていたため設定。設定しないとテスト実行時にクラッシュする。
    * Enable Bitcode: `No`
        * ※公式マニュアルに記載がないが、サンプルプロジェクトで記載されていたため設定。設定しなくても問題発生しないが念の為。

1. HelperBundleターゲット の Build Phases を以下のように設定
    * Target Dependencies に アプリ本体 を追加
    * Link Binary With Libraries に AppFramework.framework を追加し、 Statusを `Optional` に設定 <img width="1217" alt="helperbunldebuildphases.png" src="https://qiita-image-store.s3.amazonaws.com/0/36771/cc970e65-c0cd-c2cc-c30e-0defc5f07e79.png">

1. XCUITestターゲットの Build Phases で HelperBundle をアプリに組み込む
    1. XCUITestターゲットの Build Phase に移動
    1. 左上の＋ボタンを押して、 `New Copy Files Phase` を選択
    1. 追加した Copy Files Phase に以下を設定
        * Destination: `Absolute Path`
        * Path: `$(TARGET_BUILD_DIR)/../../<YOUR_APPLICATION_TARGET_NAME.app>/EarlGreyHelperBundles`
            * 必ず `EarlGreyHelperBundles` ディレクトリを指定する必要がある。EarlGreyはここを決め打ちで見に行く模様。
        * `Copy only when installing` のチェックは外す
        * 下の方にある＋ボタンを押して `<YOUR_HELPER_BUNDLE.bundle>` をコピー対象に追加。 `Code Sign On Copy` にチェックをする。 <img width="401" alt="helperbundleselect.png" src="https://qiita-image-store.s3.amazonaws.com/0/36771/adb5672c-95be-4614-48a9-958a0a74b976.png"> <img width="1218" alt="helperbundlecopy.png" src="https://qiita-image-store.s3.amazonaws.com/0/36771/95567920-fbc4-ed82-9a71-9a5da7114b44.png">

1. Bridging-Header を設定
Swiftの場合はHelperBundleでもBridging-Headerが必要となる。
設定方法はXCUITestの設定で記載したものと同じ。
設定内容は以下の通り。（TestLib関連はimportしない）

```
#import "AppFramework/Action/GREYAction.h"
#import "AppFramework/Action/GREYActionBlock.h"
#import "AppFramework/Action/GREYActions.h"
#import "AppFramework/Matcher/GREYElementMatcherBlock.h"
#import "CommonLib/DistantObject/GREYHostApplicationDistantObject.h"
#import "CommonLib/Matcher/GREYMatcher.h"
```

#### HelperBundleを使って、アプリ本体の処理を動かす

これでようやく設定周りは完了で、テストコードに取り掛かれます。
以下のファイルをHelperBundle内に新規作成します。

* SwiftTestsHost.swift
    * XCUITestに公開するプロトコル
    * HelperBundleとXCUITestの両方に追加する

```SwiftTestsHost.swift
@objc
protocol SwiftTestsHost {
  /// Obtain the host application's interface orientation.
  func interfaceOrientation() -> UIInterfaceOrientation
}
```

* GREYHostApplicationDistantObject+SwiftTestsHost.swift
    * アプリケーション側の処理を記述する

```GREYHostApplicationDistantObject+SwiftTestsHost.swift
extension GREYHostApplicationDistantObject: SwiftTestsHost {
  func interfaceOrientation() -> UIInterfaceOrientation {
    return UIApplication.shared.statusBarOrientation
  }
}
```

次に、XCTestCaseクラスを拡張します。hostコンピューティッドプロパティをextensionで生やして、SwiftTestsHostで定義されているファンクションを呼び出せるようにします。
書く場所はどこでもいいです。例ではデフォルトで追加されるテストケースファイル内に記述しています。

```WorkEarlGrey2UITests.swift
extension XCTestCase {
    /// A variable to point to the GREYHostApplicationDistantObject since casts in Swift fail on
    /// proxy types. Hence we have to perform an unsafeBitCast.
    var host: SwiftTestsHost {
        return unsafeBitCast(
            GREYHostApplicationDistantObject.sharedInstance,
            to: SwiftTestsHost.self)
    }
}
// 以下、省略
```

これで準備完了。アプリケーション側の処理を動かしてみましょう。

```WorkEarlGrey2UITests.swift
class WorkEarlGrey2UITests: XCTestCase {
    func testInterfaceOrientation() {
        continueAfterFailure = false
        XCUIApplication().launch()

        // XCUIApplication().launch() の後に
        XCTAssertEqual(host.interfaceOrientation(), UIInterfaceOrientation.portrait)
    }
}
```

## サンプルプロジェクト

EarlGrey2.0内にサンプルプロジェクトが同梱されています。
[EarlGrey/Tests/FunctionalTests](https://github.com/google/EarlGrey/tree/earlgrey2/Tests/FunctionalTests)

サンプルとしてきちんとUIテストの実行まで動作するので信頼できます。
インストールするのもマニュアルだけだと辛かったので、この設定と見比べたりして解読しました。

#### [サンプルプロジェクトの構成](https://github.com/google/EarlGrey/blob/earlgrey2/Tests/FunctionalTests/docs/setup.md)

Objective-C版もSwift版も両方入っているのですが、私がSwiftをメインで使っているのでSwift版として説明します。（Swiftを取れば、Objective-C版になると思います。多分。）

* FunctionalTestRigSwift
    * テスト対象となるアプリです。
* FunctionalSwiftTests
    * UIテストターゲットです。
* HostDOCategoriesSwift
    * HelperBundleです。

## まとめ

EarlGrey2.0ではXCUITestベースに移行したため、レコーディング機能が使えたり、UI操作やアサーションをXCUITestのスタイルで記述することもできます。
その上で本体アプリ側の処理をすることもできるようになり、XCUITestの弱点が見事に補われています。

XCUITestの限界を感じたら、ぜひ試してみてください。