[iOS] [Swift] HTTPサーバーライブラリ"Swifter"を使って、WebViewをUITestする

# 前置き

iOSアプリの開発において、
**「サーバーサイドの開発状況に左右されず、実装やユニットテストを進めたい」**
というのは、どんなアプリであっても共通課題かと思います。

URLRequestのレスポンスをスタブ化するには、**[OHHTTPStubs](https://github.com/AliSoftware/OHHTTPStubs)**という定番ライブラリがあります。

- レスポンスbodyとして、Xcodeリソースのjsonファイルを返却してくれたり
- レスポンスbodyとして、jsonオブジェクト（[String: Any]型のオブジェクト）を返却してくれたり
- レスポンスheaderを設定できたり
- ステータスコードを設定できたり

通信スタブとして必要なことは概ね揃っている、素晴らしいライブラリです。
README.mdには明記されていませんが、Xcode 10 & Swift 4でも一応利用できています（採用を検討される際はご自身で確認してくださいね！）。

しかし残念ながら、**OHHTTPStubsはWebViewの通信のスタブ化には対応していない**ようです。
[WKWebView or UIWebView support？ #227](https://github.com/AliSoftware/OHHTTPStubs/issues/227)

そんな訳で、WebView機能、**特にWebView内での特定の操作（例えばURLスキームとか）を制御しなければならない場合、
・サーバーサイドが出来上がらないと実装できないとか
・XCUITestが使えずに手動テストになってしまったりとか**
そんな課題を抱えてていました。

# 本題

色々悩んでたどり着いたのがこちらのOSSライブラリです。
**Swifter**
https://github.com/httpswift/swifter

- iOS上にHTTP Serverを立ててくれるライブラリ
- レスポンスとして、XcodeリソースのHTMLファイルを返却してくれたり
- レスポンスとして、Swiftで書いたHTMLオブジェクトを返却してくれたり

XcodeリソースのHTMLファイルを返却させることで、サーバーサイドに影響されずにWebView機能の実装/XCUITestが可能になります。

# サンプルコード

**環境**
Xcode 10.1
Swift 4.2

## テスト対象コードのサンプル

WKWebViewにWebページを表示し、あるURLスキームがリクエストされた場合はAlertを表示するという、シンプルなものです（実際にそんなことをする必要があるかどうかはさておき）。

サーバーサイドなしではXCUITestを書くことは難しいと思います。

```swift:ViewController.swift
import UIKit
import WebKit

class ViewController: UIViewController, WKUIDelegate {

    lazy var webView: WKWebView = {
        let webConfiguration = WKWebViewConfiguration()
        let webView = WKWebView(frame: .zero, configuration: webConfiguration)
        webView.translatesAutoresizingMaskIntoConstraints = false
        webView.uiDelegate = self
        webView.navigationDelegate = self
        return webView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(webView)
        // WebViewのページロード
        guard let url = URL(string: AppDelegate.urlString) else {
            return
        }
        webView.load(URLRequest(url: url))
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        webView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0.0).isActive = true
        webView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0.0).isActive = true
        webView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0.0).isActive = true
        webView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0.0).isActive = true
    }
}

extension ViewController: WKNavigationDelegate {
    /// ページ遷移
    func webView(_ webView: WKWebView,
                 decidePolicyFor navigationAction: WKNavigationAction,
                 decisionHandler: (WKNavigationActionPolicy) -> Void) {
        guard let naviUrl = navigationAction.request.url else {
            decisionHandler(.cancel)
            return
        }
        if naviUrl.scheme?.contains("hoge") ?? false {
            // URLスキームの場合：例えばAlertを表示してみる
            let alert = UIAlertController(title: "",
                                          message: "URLスキームのリンクがtapされました。",
                                          preferredStyle: .alert)
            let action = UIAlertAction(title: "OK", style: .cancel, handler: nil)
            alert.addAction(action)
            self.present(alert, animated: true, completion: nil)
            decisionHandler(.cancel)
        } else {
            decisionHandler(.allow)
        }
    }
}
```

## Swifterを制御するクラスのサンプル

環境変数に"STUB"というkeyが存在する場合、Swifterを起動し、valueをURLとして使用する、という決め事にしてみました。

```swift:StubServer.swift
import Foundation
import Swifter

class StubServer {
    static let shared = StubServer()
    private init() {}
    
    /// Stubサーバー(Swifter)のインスタンス
    let server = HttpServer()
    
    /// Stubサーバーを必要としているかの判定と、必要とする場合はStubのURLを取得する
    func getStubInfo() -> (needsStub: Bool, url: String) {
        let env = ProcessInfo.processInfo.environment
        if let value = env["STUB"] {
            // 環境変数に"STUB"というkeyがある場合、STUBサーバーを使う
            return (needsStub: true, url: value)
        }
        return (needsStub: false, url: "")
    }
    
    /// Stubサーバー(Swifter)を起動する
    func startStubServer() {
        guard let resourcePath = Bundle.main.resourcePath else {
            fatalError("ResourcePath could not get!!")
        }
        server["/:path"] = shareFilesFromDirectory(resourcePath)
        do {
            try server.start(9080)
        } catch {
            fatalError("Swifter could not start!!")
        }
    }
    
    /// Stubサーバー(Swifter)を停止する
    func stopStubServer() {
        server.stop()
    }
}
```

## AppDelegateでStubサーバーを制御するサンプル

環境変数によって、Swifterを起動させます。

URLの切り替えについては、今回は面倒臭いのでAppDelegateにURL文字列をプロパティとして宣言してしまいました。
plistを環境ごとに用意して、それを管理するclassを定義するなど、もう少し設計を工夫した方がbetterかと思います。

```swift:AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    
    /// Stubと本番でURLを動的に変更
    class var urlString: String {
        let stubInfo = StubServer.shared.getStubInfo()
        if stubInfo.needsStub {
            return stubInfo.url
        } else {
            return "https://www.apple.com/"
        }
    }

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Stubサーバー(Swifter)の起動
        if StubServer.shared.getStubInfo().needsStub {
            StubServer.shared.startStubServer()
        }
        return true
    }

    func applicationWillTerminate(_ application: UIApplication) {
        // Stubサーバー(Swifter)の停止
        if StubServer.shared.getStubInfo().needsStub {
            StubServer.shared.stopStubServer()
        }
    }
}
```

## ダミーのHTMLリソースのサンプル

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>WebViewスタブ</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <div align="center">
        <h1>スタブのHTMLです。</h1>
        <p><a href=hoge://>URLスキーム</a></p>
    </div>
</body>
</html>
```


## XCUITestのサンプル

環境変数に"STUB"というkeyにURLを設定することで、Swifterを使用するようにしています。

```swift
import XCTest

class SwifterResearchUITests: XCTestCase {
    let app = XCUIApplication()

    override func setUp() {
        continueAfterFailure = false
        app.launchEnvironment = ["STUB": "http://localhost:9080/"]
        app.launch()
    }

    override func tearDown() {
    }

    func testExample() {
        XCTContext.runActivity(named: "トップページがWebViewで表示されること") { (activity) in
            let webViewTitle = app.webViews.staticTexts["スタブのHTMLです。"]
            XCTAssertEqual(waitAppearance(for: webViewTitle), .completed)
        }
        XCTContext.runActivity(named: "URLスキームをtapしたらalertが表示されること") { (activity) in
            app.webViews.links["URLスキーム"].tap()
            let alert = app.alerts.staticTexts["URLスキームのリンクがtapされました。"]
            XCTAssertEqual(waitAppearance(for: alert), .completed)
        }
    }
}

extension XCTestCase {
    /// elementの出現を待つ
    func waitAppearance(for element: Any) -> XCTWaiter.Result {
        let exp = expectation(for: NSPredicate(format: "exists == true"), evaluatedWith: element, handler: nil)
        let result = XCTWaiter.wait(for: [exp], timeout: 3.0)
        return result
    }
}
```

## GitHub

https://github.com/y-some/SwifterResearch