Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
18
Help us understand the problem. What is going on with this article?
@watanave

ios-snapshot-test-caseを快適に運用する5つのtips

iOSアプリ開発においてViewのスナップショット画像を撮影比較する便利なツールがあります。
ios-snapshot-test-case

この記事では、ios-snapshot-test-caseを快適に運用する5つのtipsを紹介します。
またこの記事で紹介するサンプルプロジェクトはこちらにあります。

尚、このツールの概要・導入・活用術については@imaizume さんのこちらのスライドが大変学びが多くお勧めです。
https://speakerdeck.com/imaizume/practical-snapshot-testing

アニメーションを無効にする

アニメーションがあるとスナップショット画像が安定せず、画像に差分が発生 (XCTest失敗) してしまいます。
そこで、テストの時はアニメーションを無効化しておくのがオススメです。

こちらのAPIを利用します。

class func setAnimationsEnabled(_ enabled: Bool)

テストの時のみアニメーションを無効にする方法を2つ紹介します。

方法1: 実行引数

XcodeのEdit Scheme...から
スクリーンショット 2020-08-11 16.09.04.png

Testの実行引数を指定します。
文字列 disable-animations は任意でなんでも結構です。
Monosnap 2020-08-11 16-09-40.jpg

デバック実行時の環境変数を上書きしてしまうため、以下の設定も必要です。
特に $(SOURCE_ROOT) などの環境変数を展開するために Expand Variables Based Onでアプリターゲットします。

Monosnap 2020-08-11 16-54-45.jpg

AppDelegateで実行引数にdisable-animationsが含まれていたら、アニメーションを無効化します

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        if ProcessInfo.processInfo.arguments.contains("disable-animations") {
            UIView.setAnimationsEnabled(false)
        }
        return true
    }

}

方法2: テスト専用のAppDelegateを実装する

UIApplicationやAppDelegateのクラスを動的に指定するこちらのAPIを利用します。

func UIApplicationMain(_ argc: Int32, 
                     _ argv: UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>, 
                     _ principalClassName: String?, 
                     _ delegateClassName: String?) -> Int32

main.swiftというファイルを追加し、以下のように実装します。
クラス名やtarget名は適宜変更してください。
ミソはTestターゲットのAppDelegateがあればソレを採用する。なければ、アプリターゲットのAppDelegateを採用するようになっている点です。

import UIKit

UIApplicationMain(CommandLine.argc,
                  CommandLine.unsafeArgv,
                  nil,
                  NSStringFromClass(NSClassFromString("SnapshotSampleTests.AppDelegate") ?? AppDelegate.self)
)

アプリターゲットのAppDelegateから@UIApplicationMainアノテーションを削除します。

  import UIKit

- @UIApplicationMain
  class AppDelegate: UIResponder, UIApplicationDelegate {

TestターゲットにAppDelegate.swiftを追加し、アニメーションを無効化します。

import UIKit

class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        UIView.setAnimationsEnabled(false)
        return true
    }

}

言語設定

日本語のテキストを扱うときは、ローカル環境とCI環境でフォントが異なり、スナップショットに差分が出ることがしばしばあります。
そのため、テスト実行時の言語設定を施しておくと、スナップショットテストが安定します。

XcodeのEdit Scheme...から
スクリーンショット 2020-08-11 16.09.04.png

Japaneseを指定します

Monosnap 2020-08-11 16-30-38.jpg

CIの成果物に差分画像を含めておく

Bitrise等のCI環境でスナップショットテストを実行し画像に差分が発生した際に、どんな差分だったのか?そのままでは確認するすべがありません。
(CI環境でのみ画像に差分が発生することは多いです)
そこでCIの成果物にスナップショットテストの差分画像を含めておくことをお勧めすます。

例えば、bitriseでしたらこのような設定で可能です。
テスト失敗時に成果物を公開するように
is_always_run: truerun_if: ".IsBuildFailed" などを指定する必要があります。

    - xcode-test@2:
        inputs:
        - project_path: "$BITRISE_PROJECT_PATH"
        - simulator_device: iPhone SE (2nd generation)
        - scheme: "$BITRISE_SCHEME"
    - zip-directory-and-export-its-path@1:
        is_always_run: true
        run_if: ".IsBuildFailed"
        inputs:
        - include_directory: 'true'
        - directory_to_zip: "./SnapshotSampleTests/FailureDiffs"
    - deploy-to-bitrise-io@1:
        is_always_run: true
        run_if: ".IsBuildFailed"
        inputs:
        - notify_user_groups: none
        - is_enable_public_page: 'true'
        - deploy_path: "$ZIP_FILE"

スクリーンショット 2020-08-11 14.51.37.png

RecordModeの切り替えは共通にしておく

スナップショットを撮影するFBSnapshotTestCaseクラスは recordModeフラグを持っています。このフラグがtrueの場合はリファレンス画像を更新します。falseの場合は画像の差分をチェックします。

参考

SDKのメジャーバージョン更新時など、全ての画像を一括更新かけることもありますので、このフラグはグローバルな物を代入するようにしておくと便利です。
実装例

便利メソッドの作り方

dark modeとlight mode両方のスナップショットを撮影する便利メソッドの実装例です。
参考

extension FBSnapshotTestCase {

    func snapshotBothMode(_ viewController: UIViewController, file: StaticString = #file, line: UInt = #line) {
        let window = UIWindow(frame: UIScreen.main.bounds)
        window.rootViewController = viewController
        window.makeKeyAndVisible()

        FBSnapshotVerifyView(window, identifier: "light", file: file, line: line)
        window.overrideUserInterfaceStyle = .dark
        FBSnapshotVerifyView(window, identifier: "dark", file: file, line: line)
    }

}

引数にファイル名と行番号が含まれているのがミソです。
このファイル名と行番号が指定された箇所がテスト失敗として扱われます。
これは標準のXCAssertが備えている機能ですので、スナップショットテスト以外にも応用が可能です。
https://developer.apple.com/documentation/xctest/1500669-xctassert

ファイル名と行番号が指定した場合

独自メソッド snapshotBothModeの呼び出し箇所でテスト失敗となります
スクリーンショット 2020-08-11 15.01.41.png

ファイル名と行番号が指定しない場合

独自メソッド snapshotBothModeの内部でテスト失敗となります
スクリーンショット 2020-0x8-11 15.02.11.png

Dangerでスナップショットを強制する

せっかくios-snapshot-test-caseを導入しても、スナップショットを比較するテストの実装を忘れてしまうと意味がありません。

そこで、Pull-Requestの静的チェックツールDangerでスナップショットテストの実装をチェックしてみましょう。
参考

スナップショットの実装をチェックする最低限の設定をしたサンプルです。
https://github.com/watanavex/SnapshotSample/blob/e0a061a825a96d5cf5c950bac3ed77b545afcdde/Dangerfile

Pull-Requestではこのようにワーニングが発生します。
スクリーンショット 2020-08-11 15.08.15.png

18
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
yumemi
みんなが知ってるあのサービス、実はゆめみが作ってます。スマホアプリ/Webサービスの企画・UX/UI設計、開発運用。Swift, Kotlin, PHP, Vue.js, React.js, Node.js, AWS等エンジニア・クリエイターの会社です。Twitterで情報配信中https://twitter.com/yumemiinc

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
18
Help us understand the problem. What is going on with this article?