WireMockを使ったXcode UI Testing

大晦日ですね。

それはおいといて、

UIテストなどを書いている際、画面遷移をする上で通信が必要となる場面が多いかと思います。
しかしその時点でサーバが準備されてない、またはサーバが安定しないためテスト自動化で失敗するケースが増えるのを避けたいなどの理由からローカル環境にモックサーバを立てて開発・テストする事もあるかと思います。今回はそんな時に使うと便利なモックサーバ構築用のツールである`WireMock`について簡単な例を元に説明していこうと思います。

# WireMockとは
HTTPベースのAPIのシミュレータ、モックサーバーとして便利な機能を揃えています。
http://wiremock.org
https://github.com/tomakehurst/wiremock

## WireMockの起動
WireMockは一つの`jar`ファイルで提供されており、起動についてはこれを実行するだけです。
http://repo1.maven.org/maven2/com/github/tomakehurst/wiremock-standalone/2.12.0/wiremock-standalone-2.12.0.jar

```
$ java -jar wiremock-standalone-2.12.0.jar
```

これで`http://localhost:8080`でモックサーバーにアクセスできます。

※Javaの実行環境が必要になりますのでインストールしておいてください。

公式HP
http://wiremock.org/docs/running-standalone/

### クライアントライブラリ
Wiremock自身のAPIを叩くためのクライアント実装としてはWiremockClient(https://github.com/mobileforming/WiremockClient) を使用しています。

## WiremockClientの使い方

### レスポンスのマッピング
Wiremockは特定のリクエストに対して指定したレスポンスを返す設定を行うことが出来ます。

まず`WiremockClient`の`postMapping`メソッドを使ってスタブのマッピングを行います。
以下の例では`/api/v1/event/?keyword=&start=1&count=10`がリクエストされた時のレスポンスとしてテストバンドルに登録された`page1.json`が読み込まれて設定されます。

```swift
    override func setUp() {
        super.setUp()
        let bundle  = Bundle(for: type(of: self))
        WiremockClient.postMapping(stubMapping: StubMapping
            .stubFor(requestMethod: .GET, urlMatchCondition: .urlEqualTo, url: "/api/v1/event/?keyword=&start=1&count=10")
            .willReturn(ResponseDefinition().withLocalJsonBodyFile(fileName: "page1", fileBundleId: bundle.bundleIdentifier!, fileSubdirectory: nil)))

        ...
        app.launch()
    }
```

ちなみに殆どの場合、設定するレスポンスは長めにになると思いますが、テストコードで直接JSONを文字列を与える事も可能です。

### マッピング内容の破棄
`tearDown`で以下のように`WiremockClient.reset()`を実行しておけば、登録した内容は破棄されます。

```swift
    override func tearDown() {
        super.tearDown()
        WiremockClient.reset()
    }
```

## 登録内容の確認
Wiremockに登録されているレスポンスなどについて確認したい場合は
ブラウザで`localhost:8080/__admin/docs`を開き`Swagger UI`の管理画面へアクセスできるので、`GET /__admin/mappings`で確認出来ます。
![Screen Shot 2017-12-31 at 16.13.34.png](https://qiita-image-store.s3.amazonaws.com/0/11776/d899626f-da6b-ef73-10a6-4b850d4203d8.png)

# 使用例
今回、Connpass APIを使ったサンプルコードを書き試してみました。
Connpass API リファレンス
https://connpass.com/about/api/

アプリが取得するデータは以下の３つ
1. Connpass APIのレスポンス
2. ConnpassのイベントページのHTML
3. イベントページに設定されているOGP画像

※2.と3.についてはOGP画像の取得のため行なっています。

通信の内容は以下の通り
![Screen Shot 2017-12-31 at 16.54.15.png](https://qiita-image-store.s3.amazonaws.com/0/11776/c1259a4d-aafb-64df-b77f-26b60334d942.png)

アプリを普通に触ってると以下のような感じで動きます。
起動→イベント一覧取得→セルタップでイベント詳細画面へ遷移

![Dec-31-2017 15-17-10.gif](https://qiita-image-store.s3.amazonaws.com/0/11776/eb7b39fa-c456-8c9c-8736-9e418554b04e.gif)

### APIのレスポンスのみモックしてオフラインで実行してみる
レスポンスとなるjsonを準備して、Wiremockに登録し、UIテストを実行します。

今回私はテストデータの作成にはCharlesを使いましたが、WiremockにはRecodingの機能もあるため、そちらを試してみるのも良いでしょう。


```swift
import XCTest
import WiremockClient

class ConnpassViewerUITests: XCTestCase {
        
    override func setUp() {
        super.setUp()
        let bundle  = Bundle(for: type(of: self))
        WiremockClient.postMapping(stubMapping: StubMapping
            .stubFor(requestMethod: .GET, urlMatchCondition: .urlEqualTo, url: "/api/v1/event/?keyword=&start=1&count=10")
            .willReturn(ResponseDefinition().withLocalJsonBodyFile(fileName: "page1", fileBundleId: bundle.bundleIdentifier!, fileSubdirectory: nil)))

        WiremockClient.postMapping(stubMapping: StubMapping
            .stubFor(requestMethod: .GET, urlMatchCondition: .urlEqualTo, url: "/api/v1/event/?keyword=&start=20&count=10")
            .willReturn(ResponseDefinition().withLocalJsonBodyFile(fileName: "page2", fileBundleId: bundle.bundleIdentifier!, fileSubdirectory: nil)))

        continueAfterFailure = false
        let app = XCUIApplication()
        app.launchArguments.append(contentsOf: ["wiremock"])
        app.launch()
    }
    
    override func tearDown() {
        super.tearDown()
        WiremockClient.reset()
    }
    
    func testExample() {
        XCTContext.runActivity(named: "Select first event") { _ in
            let app = XCUIApplication()
            let expectation = self.expectation(description: "wait until to display cell")
            app.cells.element(boundBy: 0).tap()
            expectation.fulfill()
            waitForExpectations(timeout: 30) { (error) in
                XCTAssertNotNil(app.cells.element(boundBy: 0), "cell is not found")
            }
        }
        XCTContext.runActivity(named: "Open event url") { _ in
            let app = XCUIApplication()
            let expectation = self.expectation(description: "wait until to display cell")
            app.cells["event_url"].tap()
            expectation.fulfill()
            waitForExpectations(timeout: 3) { (error) in
                XCTAssertNotNil(app.cells["event_url"], "event url cell is not found")
            }
            app.buttons["Done"].tap()
        }
    }
}
```

UI Testの実行時のみWiremockのAPIを叩きたいので、`app.launchArguments.append(contentsOf: ["wiremock"])`で起動時の引数として`wiremock`を与え、アプリがAPIをコールする際この引数が与えられていたら`http://localhost:8080`へホストを変更する処理をアプリには加えてあります。

以下のような形です。

```
if ProcessInfo.processInfo.arguments.contains("wiremock") {
// WiremockのAPIを叩く
} else {
// Connpass APIを叩く
}
```

こんな感じのイメージですね。
![Screen Shot 2017-12-31 at 16.54.18.png](https://qiita-image-store.s3.amazonaws.com/0/11776/26a3b7a8-2838-4195-ba85-328da42c029a.png)


実行結果
![api_only.gif](https://qiita-image-store.s3.amazonaws.com/0/11776/d0eb1191-ee71-8e6e-9898-8e33f885933d.gif)

はい、APIのレスポンスは返却されているので画面遷移は問題なく行えました。
しかしOPGの画像が取得でいていないため画面はほぼほぼ真っ白です😅

### JSON以外のレスポンスを設定する
では次はOPG画像の表示のためにHTMLと画像ファイルを返すように設定します。
Wiremockは起動時に`__files`と`mappings`ディレクトリにあるファイルを読み込むため、その設定を行なっていきましょう。

以下のようにファイルを配置しておくとブラウザで`http://localhost:8080/event/71358/index.html`へアクセスする事が出来るようになります。

```
.
├── __files
│   ├── event
│   │   ├── 12345
│   │   │   └── index.html
│   │   └── 67890
│   │       └── index.html
│   └── thumbs
│       ├── 12
│       │   └── 4e
│       │       └── 124ea69e92f84e570c6bdc1736906cb6.png
│       └── 34
│           └── f2
│               └── 34f2dcb0e2f2b6b23ab08597fee230fa.png
├── mappings
└── wiremock.jar
```

用意したhtmlファイルと画像についてはJSONレスポンスと同様にCharlesで取得し、ホスト名を`http://localhost:8080`に一括置換して配置しました。

こんなイメージです。
![Screen Shot 2017-12-31 at 16.54.21.png](https://qiita-image-store.s3.amazonaws.com/0/11776/aee2fd4a-843d-de6a-fd86-0668d3e965f4.png)

実行結果
![mocked_ogp.gif](https://qiita-image-store.s3.amazonaws.com/0/11776/dbbe39d3-28c9-e5f7-3dad-92531efa79a9.gif)

OPG画像がちゃんと表示されましたね。
これでオフライン時でもきちんと画像がロードされた画面を作ることが出来るようになりました😃

# SwaggerによるAPIモックとの比較
最近はSwaggerを使ってのモックサーバの構築の話もよく聞くので簡単に比べてみました。
Swaggerは定義ファイルによってモックサーバとクライアント実装を自動生成してくれるため、Swaggerがサポートするトップダウンの開発時は非常にうまく機能すると思うのですが、今回の例に挙げたような外部APIをモックする場合、Swaggerのドキュメントなどでいわれるボトムアップによるやり方が難しいような気がしていて、そのような場合はWiremockを使うと楽になりそうな気がしています。

# 雑談
そもそもモックサーバを使ってのUIテストをやろうとしたのは以前好んで使っていた`ios-snapshot-test-case`(https://github.com/facebookarchive/ios-snapshot-test-case )がアーカイブされ、Xcode9から使えなくなった事、XCode9からXCTestが明示的なスナップショットに対応した事などを受け、FBSnapshotTestCaseをforkして使うのも考えたのですが、XCode UI TestingでUIを確認する方法へ切り替えてみようかな？と思った事がきっかけでした。そしてその時イメージしたモックサーバの動きがiOSの単体テストライブラリである`OHHTTPStubs`(https://github.com/AliSoftware/OHHTTPStubs) や`Mockingjay`(https://github.com/kylef/Mockingjay) などのように`setup`でモックするレスポンスの設定を行い、`teardown`でそれを破棄するという形にできないものかと考えて自分で`Vapor`で書き始めてたのですが、そもそもありそうな気がして調べて見つけたのが今回紹介した`WireMock`でした。概ね求めていた通りの動きでよかったのですが、複数のドメインに対して1つのWiremockサーバーでは対応しきれないという事もわかりましたが、まぁこれについてはモックサーバーの役割がそもそも自前のAPIの代替として使ったりするわけですから当然ですね。(例えばConnpassではグループごとにサブドメインが切られるのでHTMLファイルのモックするため、`--files/event/`配下に各イベントのHTMLファイルを移動させてあったりします。)

Wiremockについてはまだ多くの機能があるのでこの記事の評判良ければまたなんか書いてみようと思います。


# 参考文献
https://marcosantadev.com/run-swift-ui-tests-mock-api-server/
https://dev.classmethod.jp/etc/wiremock-practice/
https://kazucocoa.wordpress.com/2014/12/28/mockサーバ比較とwiremockメモ/
https://engineering.linecorp.com/ja/blog/detail/92