👏

Github Actionsでswift-snapshot-testingを使ったVRTを構築してみた(1/2)〜ローカル対応編

に公開

VRT(Visual Regression Testing)とは?

VRTは「Visual Regression Testing」の略で、ソフトウェアやウェブアプリケーションのユーザーインターフェースにおける、意図しないビジュアル上の変更を検知するためのテスト手法です。これにより、デザインの一貫性やユーザビリティを保ちながら、新しい変更が既存のレイアウトやスタイルに影響を及ぼしていないかを確認できます。

はじめに

この記事は、
「書いてある通りにやればVRTができる!」ではなく、
どのように検証を進めていったかの備忘録になります。

導入環境

  • XcodeGen
  • FeatureModule
  • SPM
  • Github Actions

検討・採用したもの

  • swift-snapshot-testing
    • SwiftUIのViewにも対応
    • アサーションで簡単にスクショ生成・比較が可能
  • ImageMagick
    • 差分画像の生成
  • Github Actions
    • 既存の技術スタックを踏襲
  • Github Cache
    • テストオラクルな画像はCacheに保存
    • デフォルトで7日間
    • 10GBの容量
  • Compareブランチ
    • PRに表示するための「既存・差分・今回」の画像は Compare_Feature ブランチにコミット
    • マージ後削除
    • Githubのエコシステムの中で完結させたかった

検討したがNGになったもの

  • Github Releasesに画像アップロード
    • Github CLI で唯一 upload を持ち、画像アップロードが可能だが、利用意図と異なる
  • Issueに画像アップロード
    • Github CLI に upload がない
    • 意図と異なる
    • 大元のデータの削除に、Githubサポートとの連携が必要
    • セキュリティ(以前は特定の方法で外部からアクセスできていた)
  • ビジュアライズにreg-suitの利用
    • 外部サービスに依存したくない
    • PR上で直接確認したい
  • S3にアップロード
    • 外部サービスに依存したくない
  • Github Artifacts
    • Compare_Featureブランチを採用したため
    • artifactはデフォルトで90日間保存
    • artifactごとに2GBまでの制限
    • artifactは各コミットに対して紐づいている

検証手順

業務に安全に・効率よく組み込むために、次の手順で検証していきます。

  1. 個人の公開リポジトリで最小限のプロジェクトを構築
    公開リポジトリではGithub Actionsを無料で利用可能
    非公開リポジトリでの料金について
  2. ローカル環境で対応
  3. Github Actionsのワークフロー構築
  4. チームのリポジトリに導入・ワークフロー未対応
    既存のtestplanと分けることで、一旦CIで実行されないようにしておく
  5. チームのリポジトリでGithub Actionsのワークフロー構築
    スナップショット用のテストプランを実行するワークフローを構築する

本題

swift-snapshot-testingの導入

SPM

※プロジェクトによって導入方法は様々なので、詳細は省きます。
swift-snapshot-testingより、latestなバージョンを指定

SnapshotTesting:
  url: https://github.com/pointfreeco/swift-snapshot-testing
  version: 1.18.1

テストコード

最小限の、スナップショット生成・比較コードを実装

  1. テストオラクルな画像がない場合は保存のみ行い、テストは失敗する
    保存先はデフォルトでcurrent directoryを元にパスが設定される
    指定したい場合は、assertSnapshotのfileパラメータにStaticStringを渡す
  2. テストオラクルな画像がある場合は比較を行い、tmpディレクトリ配下に失敗したスクショが保存される
import Testing
import SwiftUI
import SnapshotTesting
@testable import Snapshot

struct SnapshotTests {
    @MainActor
    @Test func example() async throws {
        withSnapshotTesting(diffTool: .ksdiff) {
            assertSnapshot(
                of: Text("テスト").referenceFrame(),
                as: .wait(
                    for: 0,  // スクショまでの時間
                    on: .image(
                        precision: 0.9999,  // 一致率
                        layout: .fixed(
                            width: 375, // ハードコードすみません
                            height: 667
                        )
                    )
                ),
                record: false  // 既存のスナップショットと比較する場合はfalse
            )
        }
    }
}

extension SwiftUI.View {
    fileprivate func referenceFrame() -> some View {
        self.frame(width: 375, height: 667)
    }
}

referenceFrameは以下を参考:
https://qiita.com/shxun6934/items/c97d16865a68b42b1d83

差分画像の保存先

デフォルトの保存先がまあ遠いので、任意のパスに設定したい

/Users/username/Library/Developer/XCTestDevices/05D7B9FD-8017-4664-8932-ECFB7983480A/data/Containers/Data/Application/CC88D99C-BEA5-40EE-B9CE-42B9DE0408D2/tmp/SnapshotTests/example.1.png

失敗したスクショの保存先を変えたい場合は、EnvironmentVariablesにSNAPSHOT_ARTIFACTSをキーとしてパスを設定する

弊プロダクトではXcodeGenで設定し、反映されるもテスト実行時に空になってしまったため
setenv(A, B, 1)で直接セットするようにした

@Test func example() async throws {
    withSnapshotTesting(diffTool: .ksdiff) {
        setenv(SnapshotPathConfig.environmentKey, SnapshotPathConfig.path(file: #file), 1)
        ...
    }
}
public enum SnapshotPathConfig {
    // schemeのenvironmentVariablesに設定してもテスト実行時に反映されないため、ここで設定する
    public static let environmentKey = "SNAPSHOT_ARTIFACTS"
    public static func path(file: String = #file) -> String {
        テストコードのパスをもらい、共通のFeatureモジュールをルートとして
        "Snapshots/Failure"のパスを末尾にくっつけて返す
    }
}

※注意点
テストオラクルな画像と、比較元の画像のディレクトリを分けないとcopy bundle resourcesで画像名が重複してエラーになる

.gitignoreに生成物のパスを追加

1行目はEnvironmentVariablesで設定した失敗画像のパス
2行目はswift-snapshot-testing側の基準画像のパス
ディレクトリを分けないと、bundle resourcesで名前が衝突する

Hoge/Foo/**/Snapshots/**
Hoge/Foo/**/SnapshotTest/**

チームのプロダクトに移植する

※この辺りも設定方法が様々なので割愛

スナップショット用のスキーム・テストターゲットを追加

...省略
sources:
  - path: Hoge/../SnapshotTest

スナップショット用のtestplanを追加

スナップショットのテストは時間がかかるため、ワークフロー側で微調整するためにテストプランを分けておくために追加する

既存のテストプランと分ける

XcodeGenのスキーム設定 > testPlans に追加したテストプランのパスを設定

test:
  testPlans:
    - path: Hoge/../UnitTest.xctestplan
      defaultPlan: true
    - path: Hoge/../SnapshotTest.xctestplan

fastlane側でUnitTestを実行するようにしているので、ここまでの設定で
CIがUnitTestのみ実行するようになっている。
もしそうでなければ追加の対応が必要かもしれない。

ローカル対応編・完! 次回はワークフロー構築編

これで一旦、チームのリポジトリに導入しても最小限の影響範囲に留めることができました。
次回の記事ではGithub Actionsのワークフロー構築も含めた内容で書いていきます。

Discussion