🕌

swift-snapshot-testingとPrefireで低コストでVRTを運用する

に公開

こんにちは!株式会社PREVENTでiOSアプリエンジニアの佐藤です。
この記事はPREVENTアドベントカレンダーの11日目の記事です。

https://adventar.org/calendars/12152

弊社のiOSプロジェクトで、最近swift-snapshot-testingPrefireというOSSを使ってVRTを運用し始めたのですが、低コストで運用できてかなり良かったので、紹介します!

VRTとは

Visual Regression Testの略でVRTと呼ばれており、日本語訳だと、視覚的回帰テストになります。
もう少し噛み砕くと、GUIアプリのUIにデグレがないかを検証するテストです。
モバイルアプリにおいては、コード修正前と後の画面のsnapshotを撮って、画像比較をして意図しない差分が出ていないかを見ることで、UIにデグレがないかを検証します。(あまり詳しくないですが、多分Webアプリとかも同じ手法でVRTを行うのだと思います。)

単体テストだと、UIまで検証できずロジックの部分のみの検証に留まりますが、VRTを活用すればUIのレイヤー以下のコードを統合した挙動を検証することができます。
UIのデグレを人の目で検知するのは、かなり難しくかつコストもかなりかかるのは、GUIアプリケーションを開発していれば一回は思ったことあるのではと思います。
iOS開発の文脈であれば、Previewの機能によりUIがどうなっているのかというフィードバックはかなり簡単に行えるようになりましたが、それでも一画面づつ見てくのは辛いです。
よってUIレベルの不具合は開発後半に見つかり、修正で手戻りすることも少なくないのではと思いますが、VRTが自動化できればそのようは不具合も実装の段階で早期に発見できるようになります。

OSSの紹介

今回紹介するVRTの運用に必須のOSSを紹介します。

swift-snapshot-testing

一つ目は、swift-snapshot-testingというOSSです。
このOSSがやってくれるのは、主に以下2点です。

  • Viewからsnapshotを生成する
  • 既存のsnapshotと生成したsnapshotの比較をして差分があればテスト失敗をthrowする

swift-snapshot-testingを使ってViewのsnapshotを比較する単体テストを実装することができます。
今回紹介する運用では、swift-snapshot-testingでVRTが行える単体テストの実装を基本的には意識しなくてもいいので詳細には触れませんが、気になる方は公式のドキュメントか関連のテックブログもあるので調べてみてください。

Prefire

二つ目は、PrefireというOSSです。
このOSSは、先に紹介したswift-snapshot-testingの拡張Pluginみたいな立ち位置で、開発者が定義したPreviewからswift-snapshot-testingのテストケースを自動生成してくれます。

以下のようなPreviewがあったら、Previewに表示されるUIのsnapshotを比較するVRTのケースを自動で生成してくれるのです。

#Preview {
    Text("Hoge")
}

自動生成されるテストケース

func test_TestView_0_Preview() {
        let preview = {
            Text("Hoge")
        }
        let isScreen = true
        if let failure = assertSnapshots(for: PrefireSnapshot(preview(), name: "UserProfileView_1", isScreen: isScreen, device: deviceConfig)) {
            XCTFail(failure)
        }
    }

Prefireのテストケース自動生成機能により、開発者は基本的にVRTを行いたい画面のPreviewを実装するだけでよく、非常に低コストでVRTを作ることができます。

Prefireのセットアップ手順などは、公式のREADMEで詳しく説明されているので、そちらをご参照ください。

運用例

紹介した二つのOSSを使ってどのようにVRTを運用しているのかについて、説明します。
VRT実行の流れとしては以下のとおりです。

  1. Viewの実装・修正を行う
  2. Viewを実装するときにVRTで検証したいUIを表示するPreviewを定義する
  3. ローカル環境でVRTのテストを実行する
  4. 差分が出ているケース、もしくは新しく追加されたViewのケースはエラーが出る※(エラーが出なければ手順6へスキップ)
  5. 差分が出ているケースが意図した差分であれば、古いsnapshotを削除して手順3に戻る
  6. 修正内容をPush
  7. CIのワークフローでVRTを含むテストを実行
  8. CIが通れば、VRTが問題ないことを確認できる

運用の全体像を図示すると以下のような流れです。

上記の運用を行うことで、PRを作る段階で、意図しないUIの差分が出ていればすぐに検知することができるようになります。
また、開発者が上記の手順を正確に覚えていなくとも、CIでVRTを行うのでもし差分が出ていれば必ずCIが失敗し、リポジトリには常に最新のUIのsnapshotが配置されるようになっています。
Viewを修正したPRを作るときは、毎回snapshotを更新するのが、この運用でやや負担になるところですが、PRのスコープを適切に区切ったり(Viewとロジックの修正を分ける)減らす(差分は500行まで見たいなルールを設けるなど)ことで毎回の修正で大量のsnapshotを更新することはなくなるので、PRのスコープルールと併用すれば、現実的な運用コストに収まると思います。

運用上の課題

課題として、ローカルではVRTが通るのにCIでは通らないといったことが発生する場合があります。
この原因の多くはローカル環境とCI環境でのSimulatorの環境設定の違いに起因しています。
例えば、時間を扱うViewの場合は、Localeが実行環境で変わる場合があり、自ずと出力されるsnapshotの内容も変わってしまったりします。
前提として、VRTようにPreviewを実装する時はそういった環境変数を固定するために、environmentのmodifierを使うなどして固定のLocaleを注入しておくなどの対策はしておくべきです。
ただ、それでも別の要因でCIでVRTが通らないという場合があるので、CI環境でどのような差分が発生しているのかは見れるようにしておいた方が便利です。

swift-snapshot-testingのテストケースで失敗した場合、画像の差分をテスト結果から確認することができます。
Xcodeでは以下のように確認することができます。

変更前 変更後

XcodeでVRTのテスト結果を見た時、以下のように差分箇所が白くなっていることがわかります。

xcodebuildでテストを実行するときに、-resultBundlePathでパスを指定するとそのパスに〇〇.xcresultというテスト結果ファイルを生成するので、これをCI環境でアーティファクトとしてアップロードしておくと、CI環境でVRTが失敗した場合も、xcresultファイルをローカルでXcodeで開いて、上記のように差分を確認できるようにしておけば、調査がグッと簡単になるのでおすすめです。

まとめ

VRTを低コストで運用する方法について、紹介しました。
今回はVRTの運用の紹介に焦点を当てましたが、どこかでPrefireの詳しい使い方も紹介しようと思います!

本記事のまとめとしては以下のとおりです。

  • PrefireでPreviewの定義からswift-snapshot-testingのテストケースを自動生成することで、簡単にVRTのテストケースを実装できます。
  • これをCIでも実行することで、PR毎にUIのデグレがないかを検知でき、UIのデグレに関連した開発後期の手戻りを減らすことができます。

Previewの結果がいつでも同じになるようにViewからプロセス外依存が疎結合になっていれば、紹介した運用の導入は比較的簡単なので、試しにやってみてもらえればと思います!

Discussion