Visual Regression Testingを導入してみた(iOSアプリ)

この記事は[レコチョク Advent Calendar 2022](https://qiita.com/advent-calendar/2022/recochoku)の10日目の記事となります。

https://qiita.com/advent-calendar/2022/recochoku

# はじめに

最近学生時代の友人と会い、昔やっていたギターの熱が再熱してきました村田です。
株式会社レコチョクでiOSアプリ開発をしています。

Visual Regression Testing(以下VRT)をタワーレコード株式会社と弊社が共同で開発してる
TOWER RECORDS MUSICという音楽サブスクリプションサービスのiOSアプリに導入したので、
それを紹介しようと思います。

https://music.tower.jp/

# VRTとは？

特定の時点でのUIのスクリーンショットをピクセル単位で比較し、差分を検知する回帰テストです。

CIと連携してPR毎に自動でテストを実行することでデグレの検知ができるため、非常に高い効果を発揮します。

# 導入検討時の課題は？

機能エンハンス時に、画面に表示されるデータの一部の表示順がエンハンス前と異なってしまった事がありました。
エンハンスの内容的には直接関係ない部分でしたが、表示順に影響がある箇所を触っていたようです。怖いですね。
こういったことは人の目をすり抜けてリリースされてしまうリスクがあります。
そのため、可能な限り自動で検知したいと思ったことが導入検討の一番のきっかけでした。

上記の問題はオブジェクトに対するユニットテストでも検知することが可能です。
しかし、当時のアプリの設計は上記のテストコードを書くことを考慮した設計になっていなかったため、それによる解決が困難でした。

そこで、それを解決する方法としてVRTが候補に上がりました。

# どんな運用？

以下の流れで運用をしています。

- VRTのテストケースを実装
- 正解とする画像をテストを実行して撮影し、コミット
- Pull Requestが作成されたタイミングでテストをCI上で実行

テスト実行時の全体のアーキテクチャはこのようなイメージです。
Unit Testing BundleとしてVRTを実装するため、ユニットテストを実行としています。

![architecture.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201186/749ce517-8e08-1c0e-e85d-f4662b8cf47e.jpeg)


UIや表示されているデータ等の画面表示に関わる処理が変更された場合は、正解とする画像を作成してコミットします。
正解とする画像をコミットしているため、変更があるとこのようにGitHub上で画像の差分が見えます。
Pull Requestを作成したときによくやるような「対応の前後のスクリーンショットを撮ってレビュワーに対応内容をわかりやすくする作業」を別途やらずに済むこともあり、開発効率も上がったりします。

![git_diff.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201186/cb54b2d9-025a-362a-f11d-9d1622019a46.png)


# 実際テストコードは？

UIのスクリーンショットの取得と比較の部分は[iOSSnapshotTestCase](https://github.com/uber/ios-snapshot-test-case)を使用しました。
HTTP通信も行うため、[OHHTTPStubs](https://github.com/AliSoftware/OHHTTPStubs)も使用しています。

[swift-snapshot-testing](https://github.com/pointfreeco/swift-snapshot-testing)がよく比較に上がると思いますが、[iOSSnapshotTestCase](https://github.com/uber/ios-snapshot-test-case)は差分があった場合に、視覚的に理解できるDiff画像を一緒に作ってくれるところが手軽で良いと思い選定しました。
現在の運用ではテスト失敗時にCIからダウンロードできるようにしています。
ダウンロードできる画像は以下のものになります。

![image_diff.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201186/a0b0041a-6e01-8b64-9006-0f2117c8b437.jpeg)


実際のコードは以下になります。
とあるアプリのホーム画面の1テストケースです。
WebAPIから情報を取得し表示と、UserDefaultsの情報によってUIが変化する機能があります。

```swift
final class HomeViewControllerSnapshotTest: FBSnapshotTestCase {
    var subject: UIViewController!

    override func setUp() {
        super.setUp()

        subject = (インスタンス化)

        // 正解となる画像を取るフラグで、画面に変更があった場合はtrueにしてテストを実行して画像をコミットする
        recordMode = SnapshotTest.recordMode

        // 必要に応じてDBやスタブ周りの設定
    }

    func test_Home_未ログイン_全項目存在() throws {
        // UserDefaultsやDBのデータをテストケースに合わせて変更
        Settings.Application.userState = .notLoggedIn

        // HTTP通信がある場合のスタブ
        stub(condition: isPath("APIのPath")) { _ in
            fixture(
                filePath: OHPathForFile("レスポンスとする自前で用意したJSONのファイル名", type(of: self))!,
                headers: ["Content-Type": "application/json"]
            )
        }

        // スナップショットを取るための準備も含んで行う拡張メソッド
        // Viewのライフサイクルの呼び出しや遅延処理などをしています
        snapshotTest(
            subject: subject,
            frame: .init(x: 0, y: 0, width: 375, height: 3500), // 画面のすべての要素が表示できるサイズ
            overrideUserInterfaceStyle: .dark
        )
    }
}
```

別途setUpやtearDownメソッドでDB等のデータの前準備は必要ですが、テストケース毎に行うことしては下記の2つを書くだけです。
- データの取得先のWebAPIやDBのスタブ化や実際の値を変更
- 画面を表示させるために必要なViewの操作

非常にシンプルですね。

# なぜVRTが良い？

一般的なネイティブアプリは「WebAPIか内部のDBのデータソースからデータを取得して表示する」といった挙動になるかと思いますが、そのデータソースが大きく変わることはめったに無いのではないでしょうか。
画面や機能が変わったとしても、テストコードとして対応することは「正解となる画像を再度撮影してコミットする」だけです。
もちろん、データソースの部分や、画面を定義するViewController等のクラスが置き換わる場合はテストコードにも対応が必要ですが、それ以外のアプリの設計の影響はほとんど受けません。

また、ネイティブアプリの中心的なロジックはデータソースから取得と、取得したデータの整形になると思いますが、画面に表示されるまでにそのロジックも含まれているため、テストとして検知できる範囲も非常に広いです。
導入が容易でかつ壊れにくいテストとして運用でき、検知できる部分も多いことは非常に大きいメリットです。
画面の作りやアプリの設計がレガシーでリファクタリングをしたい！といったときにも非常に相性が良いですね。

# VRTって最高なの？

そんなVRTですが、万全ではありません。
VRTでできることはあくまで画面のスクリーンショットでの差分検知なので、動的な動作をテストすることはできません。
たとえば、細かいアニメーションの挙動や、弊社のサービスで多い音楽や動画の再生周りはVRTでテストはできていません。
これらは別途人間がテストをすることも含めてケアして行く必要があります。

また、レイアウトを表示させる必要があるため、単純にロジックを検証するユニットテストと比較すると時間がかかります。
弊社の実績では、1ケース1.2~1.3秒ほどかかっています。
テストケースが増えるとテストの実行時間も増えるため、どういったタイミングでテストを実行するかは考える必要があります。

# まとめ

導入が容易で壊れにくいテストとして運用できるVRTですが、ネイティブアプリのすべての部分に対して有用ではなかったり、時間がかかったりと欠点もあります。
しかし、表示するまでのネイティブアプリとしてのロジックのデグレ検知をしたい方や、今後リファクタリングを考えている方にはだいぶ有用なのではないかと思っています。
導入の参考になれば幸いです。

最後まで読んでいただきありがとうございました。

明日の[レコチョク Advent Calendar 2022](https://qiita.com/advent-calendar/2022/recochoku)は11日目 Spring FrameworkのDI となります。お楽しみに！

#

この記事はレコチョクのエンジニアブログの記事を転載したものとなります。

https://techblog.recochoku.jp/8669