iOS
Quick
Nimble
FBSnapshotTestCase
OriginalClassiDay 2

Nimble-SnapshotsでiOSアプリのViewをテストする

この記事は Classi Advent Calendar 2017 の2日目の記事です。

ClassiでiOSエンジニアをしている @enomotok_ です。
Nimble-Snapshotsを使ってiOSアプリのViewのリグレッションテストを行う方法を紹介します。

モチベーション

アプリのリライト

Classiにはいくつかのアプリがあります。そのなかには、過去の経緯によりアプリの内部実装が好ましくなく、リライトを行なっているものがあります。見通しの悪いコードを少しずつ改善していくわけですが、ユニットテストが整備されておらず、そのまま手を入れるとデグレが発生する可能性があります。そこでViewのレイアウトが壊れていないことを確認するために、Nimble-Snapshotsを使ってOutside-Inのテストケースを用意することにしました。

Nimble-Snapshots

Nimble-Snapshots は、 Quick のテストケースで使用するマッチャーで、Viewのスナップショットを取得したり、見た目が変化していないことをアサートすることができます。これはXCTest用に開発されたFacebook製の FBSnapshotTestCase のラッパーです。

なぜXCTestではなくQuickを使うのか

XCTestは非同期処理のテストを行うのが得意ではありません。 UIViewController のライフサイクルメソッドをコールし、非同期でWeb APIから取得した結果をViewに反映したのちに画面の見た目を検証する、といったユースケースにはQuickが適しています。Quickのマッチャーである Nimble に用意された toEventually() メソッドを使うことで、タイムアウトが発生するか、成功するまで、アサーションをコード内で繰り返し実行することができます。

使用方法

導入は簡単です。CocoaPodsでQuick, Nimble, Nimble-Snapshotsをテストターゲットに追加します。

target 'SnapshotTestingSandbox' do
  use_frameworks!
  target 'SnapshotTestingSandboxTests' do
    inherit! :search_paths
    pod 'Quick'
    pod 'Nimble'
    pod 'Nimble-Snapshots'
  end
end

スナップショットはデフォルトの設定で $(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages に保存されます。もし変更したければ、環境変数 FB_REFERENCE_IMAGE_DIR にパスを指定することで任意のディレクトリに取得したスナップショットを保存することができます。

テストコードの書き方

テストコードを書きます。
最低限のサンプルは以下の通りです。

ViewControllerTests.swift
final class ViewControllerSpec: QuickSpec {
    override func spec() {
        var sut: ViewController!
        describe("ViewController") {
            let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
            sut = storyboard.instantiateInitialViewController() as! ViewController

            it("view exists") {
                expect(sut).notTo(beNil())
                // スナップショットを取得
                //📷(sut)
                // 非同期でスナップショットがあることを確認
                expect(sut).toEventually(haveValidSnapshot())
            }
        }
    }
}

📷(sut)

スナップショットを取得するメソッドです。実行すると必ずテストは失敗します。

expect(sut).toEventually(haveValidSnapshot())

取得済みのスナップショットと、以前取得したスナップショットが等しいことを確認するアサーションです。一度テストを走らせ、 📷 () メソッドを使ってスナップショットを取得してから、このメソッドに差し替えると良いでしょう。

テストダブルについて

ViewControllerは起動時に他のオブジェクトを伴ったり、Web APIにアクセスした結果を表示するものがほとんどでしょう。ViewControllerのインスタンス化に必要なオブジェクトを一から生成するのは骨が折れる作業かもしれません。それらを別のもので肩代わりさせたい場合、テストダブルを用意したり、 OHHTTPStubs を利用したりします。今回の記事では割愛します。

サンプルコード

サンプルコードを下記のリポジトリで公開しています。
https://github.com/k-enomoto/SnapshotTestingSandbox

Objective-CでもSpecta, Expecta+Snapshotsの組み合わせで同様のテストを書くことができます。
https://github.com/k-enomoto/SnapshotTestingSandboxObjC

所感

インターネットを見回しても、iOSでOutside-Inのテストを書く知見は意外と共有されていない気がします。もし日常的にそのようなやり方で開発をしているよ、という方がいたら、是非とも色々教えていただきたいです。この記事に対する指摘も歓迎します。

最後に

Classiでは一緒に働くエンジニアを募集しています。
https://www.wantedly.com/companies/classi

明日は @cl-sawada さんです。