大晦日ですね。
それはおいといて、
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
が読み込まれて設定されます。
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()
を実行しておけば、登録した内容は破棄されます。
override func tearDown() {
super.tearDown()
WiremockClient.reset()
}
登録内容の確認
Wiremockに登録されているレスポンスなどについて確認したい場合は
ブラウザでlocalhost:8080/__admin/docs
を開きSwagger UI
の管理画面へアクセスできるので、GET /__admin/mappings
で確認出来ます。
使用例
今回、Connpass APIを使ったサンプルコードを書き試してみました。
Connpass API リファレンス
https://connpass.com/about/api/
アプリが取得するデータは以下の3つ
- Connpass APIのレスポンス
- ConnpassのイベントページのHTML
- イベントページに設定されているOGP画像
※2.と3.についてはOGP画像の取得のため行なっています。
アプリを普通に触ってると以下のような感じで動きます。
起動→イベント一覧取得→セルタップでイベント詳細画面へ遷移
APIのレスポンスのみモックしてオフラインで実行してみる
レスポンスとなるjsonを準備して、Wiremockに登録し、UIテストを実行します。
今回私はテストデータの作成にはCharlesを使いましたが、WiremockにはRecodingの機能もあるため、そちらを試してみるのも良いでしょう。
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を叩く
}
はい、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
に一括置換して配置しました。
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