特定のUIViewControllerを直接呼び出してsnapshotを高速化する。


先日iOSDCに個人スポンサーとして参加して来ました、いやー最高でしたね。毎年あってほしい。

非常に興味深い発表ばかりで刺激たっぷり、そこで得た知見なりを活かしたいと考えていたのですが、
ちょうどいいネタが@dealforestさんが発表していました。
以下はその資料

https://speakerdeck.com/dealforest/xcode-tekuai-shi-natehatukuraihuwozhui-iqiu-meru


この発表の中で個人的にグッと来たのが「特定のUIViewControllerを直接呼び出す」というもので、
発表ではXcodeからデバッグ実行時のみ特定画面を呼び出したい際に使用するという事でした。

###🤔

この発表を聞いて、ふと...

###XCUITestでもこのTipsを活かせるのではないか？

と思ったわけです。

例えばXCUITestでE2Eテストの様に画面遷移を伴ってステータスを整える事なく、
実行前に状態を与えた上で画面を呼び出し、コンポーネントの表示のみをチェックするなんて事が出来るのでは？と...

#早速試してみました

今回はfastlaneのsnapshotを使う例を作成してみました。

以下今回作成したサンプルです。


```swift
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        let arguments = NSProcessInfo.processInfo().arguments
        if let rootVcOptionIndex = arguments.indexOf("-rootViewController") {
            let rootVcId = arguments[rootVcOptionIndex+1]
            if rootVcId.isEmpty == false {
                if let vc: UIViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier(rootVcId) {
                    self.window?.rootViewController = vc;
                }
            }
        }
        
        if let appStatusOptionIndex = arguments.indexOf("-appStatus") {
            let status = arguments[appStatusOptionIndex+1]
            if status.isEmpty == false {
                self.appStatus = AppStatusType(rawValue: status)
            }
        }
        if self.appStatus == nil {
            self.appStatus = AppStatusType.LOGOUT
        }
        return true
    }
```
元ネタのTips同様、`AppDelegate`の`didFinishLaunchingWithOptions`に少々仕込む必要があります。
今回作ったサンプルでは`-rootViewController`でルートビューコントローラのID、`-appStatus`で起動時のアプリのステータスを与えられるようにしました。

パラメタの与え方については、今回使うfastlaneのsnapshotで与えられてる引数のルールなどに合わせた形にしてあります。（引数の値のパース処理については適当に書いたので汚くてもあまり気にしないでください）

ちなみにfastlaneのsnapshotでUIテストを実行した場合のargumentsの内容以下の様な形になってました。
`setupSnapshot`の中で設定されます。

```
UITestExample.app/UITestExample", 
"-AppleLanguages", 
"(jp-JP)", 
"-AppleLocale", 
"\"jp-JP\"", 
"-FASTLANE_SNAPSHOT", 
"YES", 
"-ui_testing"]
```

そして次にテストコード側の実装です。
まずは普通にsnapshotを使う場合は以下の様な実装になるかと思います。

```swift
import XCTest

class UITestExampleUISnapshotTests: XCTestCase {
    
    override func setUp() {
        super.setUp()
        continueAfterFailure = false
        let app = XCUIApplication()
        setupSnapshot(app)
        app.launch()
    }
    
    override func tearDown() {
        super.tearDown()
    }
    
    func testSnapshot() {
        
        let app = XCUIApplication()
        app.textFields["login_id"].tap()
        app.textFields["login_id"].typeText("goal-star")
        app.secureTextFields["login_pass"].tap()
        UIPasteboard.generalPasteboard().string = "abc"
        app.secureTextFields["login_pass"].doubleTap()
        app.menuItems["Paste"].tap()
        app.buttons["login_button"].tap()
        app.buttons["next_announce"].tap()
        app.textViews["text_apology"].tap()
        app.textViews["text_apology"].typeText("ごめんぽ")
        app.buttons["submit"].tap()
        snapshot("topView")

        app.buttons["next_top"].tap()
        app.buttons["ok"].tap()
        app.buttons["next_1"].tap()
        app.buttons["next_2"].tap()
        snapshot("lastView")
        
    }


}

```
ざっくり言うと起動→画面遷移→特定の画面で`snapshot("ファイル名")`を実行の流れですね。

今度は先述した`didFinishLaunchingWithOptions`に実装した機能を使って、特定のViewControllerを直接呼び出す方法です。

```swift
import XCTest

class UITestExampleUIRapidSnapshotTests: XCTestCase {
    override func setUp() {
        super.setUp()
        continueAfterFailure = false
    }
    
    override func tearDown() {
        super.tearDown()
    }

    func testExample() {
        
        var app = XCUIApplication()
        setupSnapshot(app)
        app.launchArguments.appendContentsOf(["-rootViewController", "top", "-appStatus", AppStatusType.LOGIN.rawValue])
        app.launch()
        snapshot("topView")
        app.terminate()
        
        app = XCUIApplication()
        app.launchArguments.appendContentsOf(["-rootViewController", "content_3", "-appStatus", AppStatusType.LOGIN.rawValue])
        app.launch()
        snapshot("lastView")
        app.terminate()
    }

}
```
起動→`snapshot("ファイル名")`→終了→起動→`snapshot("ファイル名")`→終了という流れになります。

起動と終了を繰り返す所はちょっとイケてなさがあるかもしれないですが、キャプチャしたい画面に到達するまで画面遷移に時間がかかるものほど効果はありそうです。

#まとめ
- 特定ViewControllerを一発呼出を繰り返してsnapshotを高速化する事は出来そう
- 今回の例の様に作り自体が状態を与えられる作りになっている必要がある

サンプルコード https://github.com/tamaki-shingo/RappidSnapshot