Merpay Advent Calendar 2019 の16日目は、メルペイ iOS チームの @akifumi がお送りします。
メルペイ iOS の品質向上を目的に、スナップショットを用いたテストを行うことができる iOSSnapshotTestCase を導入した話について記載します。
目次
- iOSSnapshotTestCaseの導入方法
- スナップショットの作成
- スナップショットテスト
- CircleCIのartifactsに失敗した画像を保存
- CircleCIのWorkflow設定
- p.s.
iOSSnapshotTestCaseとは
iOSSnapshotTestCase はUIView
やCALayer
のスナップショット画像を作成し、画像を比較することでテストを行うライブラリです。
iOSアプリを開発する中で、ユニットテストは導入しやすいですが、UIテストは難しいことが多くあります。
XCUITest
の登場によりUIテストを作成できるようになりましたが、テストのメンテナンスが困難なことによってテストが失敗されたまま放置されてしまうことが散見されます。
スナップショットテストは UIViewController
や UIView
の画像を作成するだけなので、メンテナンスを用意にすることが可能です。
またGithub上で画像を管理することで、PM・Designer・QAの方に共有することができるという利点もあります。
本記事では、メルペイにおいてどのように iOSSnapshotTestCase
を導入したかについて記載したいと思います。
iOSSnapshotTestCaseの導入方法
導入準備
メルペイ iOS チームでは、メルカリアプリに対してメルペイの機能を提供するために merpay-ios-sdk
というプロジェクトを開発しています。
merpay-ios-sdk
は、メルペイの機能を提供する複数のフレームワークで構成されています。
全てのフレームワークに対してスナップショットテストを適応するために、 Snapshots
というスナップショット専用のプロジェクトとアプリを追加しました。
インストール
iOSSnapshotTestCase
は Snapshots
のテストターゲットである SnapshotsTests
に CocoaPods
を使用してインストールしました。
Podfile
target 'SnapshotsTests' do workspace 'Merpay' project 'Snapshots/Snapshots.xcodeproj' pod 'iOSSnapshotTestCase' end
SnapshotsTests
で全てのフレームワークに対してスナップショットテストを実装しています。
導入のながれ
iOSSnapshotTestCase
でテストを行う場合は次のような導入手順が必要です。
- 環境変数
SNAPSHOT_TEST_RECORD_MODE
をtrue
にし、テストを実行する。するとテスト対象の画面のキャプチャを撮影される。 - 環境変数
SNAPSHOT_TEST_RECORD_MODE
をfalse
にし、テストを実行する。テスト対象の画面が1で撮影された画像と比較される。もし画像が異なる場合、環境変数IMAGE_DIFF_DIR
で指定したディレクトリに比較画像が保存される。
環境変数の定義
SnapshotsTests
スキーマで以下の環境変数を定義しました。
SNAPSHOT_TEST_RECORD_MODE
は参考画像を作成する際に使用します。
Name | Value | Description |
---|---|---|
FB_REFERENCE_IMAGE_DIR | $(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages | テスト時に参照する画像のディレクトリ |
IMAGE_DIFF_DIR | $(SOURCE_ROOT)/$(PROJECT_NAME)Tests/FailureDiffs | テスト時にレファレンス画像と異なる場合、差分画像が保存されるディレクトリ |
SNAPSHOT_TEST_RECORD_MODE | $(SNAPSHOT_TEST_RECORD_MODE) | レコードモードがtrueなら画像が作成され、falseならテストが実行される |
<EnvironmentVariables> <EnvironmentVariable key = "FB_REFERENCE_IMAGE_DIR" value = "$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages" isEnabled = "YES"> </EnvironmentVariable> <EnvironmentVariable key = "IMAGE_DIFF_DIR" value = "$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/FailureDiffs" isEnabled = "YES"> </EnvironmentVariable> <EnvironmentVariable key = "SNAPSHOT_TEST_RECORD_MODE" value = "$(SNAPSHOT_TEST_RECORD_MODE)" isEnabled = "YES"> </EnvironmentVariable> </EnvironmentVariables>
スナップショットの作成
SnapshotTestCase
を作成し、このクラスを継承してスナップショットテストを作成するようにしています。
SnapshotTestCase.swift
import FBSnapshotTestCase class SnapshotTestCase: FBSnapshotTestCase { var window: UIWindow! override func setUp() { super.setUp() recordMode = ProcessInfo().environment["SNAPSHOT_TEST_RECORD_MODE"] == "true" fileNameOptions = [.device, .OS, .screenSize, .screenScale] window = UIWindow(frame: UIScreen.main.bounds) window.makeKeyAndVisible() } }
UIViewControllerのテスト例
下記は、メルペイ画面のスナップショットテストの例です。
import XCTest @testable import MerpayMercariWalletKit import FBSnapshotTestCase class DashboardViewControllerTests: SnapshotTestCase { func testDashboardSnapshot() { let service = MockService() let dependencyRegistry = MockMerpayDependencyRegistry() dependencyRegistry.service = service let vc = DashboardViewController(input: .init(), dependencyRegistry: dependencyRegistry) FBSnapshotVerifyView(vc.view) } }
ViewController
を作成し、FBSnapshotVerifyView
で UIView
を渡すことでスナップショットの作成・テストを行うことができます。
CALayerのテスト例
FBSnapshotVerifyLayer
を使用し、 UIWindow
の CALayer
をでスナップショットすることで、全画面ではない画面の表示テストを行うこともできます。
import XCTest @testable import MerpayMercariWalletKit import FBSnapshotTestCase class BankRechargeNavigationControllerTests: SnapshotTestCase { private var vc: ViewController! override func setUp() { super.setUp() vc = ViewController() window.rootViewController = vc } override func tearDown() { window.rootViewController = nil vc = nil super.tearDown() } func testBankRechargeNavigationController() { let exp = expectation(description: "open BankRechargeNavigationController") let dependencyRegistry = MockMerpayDependencyRegistry() let bankRechargeNavigationController = BankRechargeNavigationController(input: .init(chargeType: .selectable, completion: nop), dependencyRegistry: dependencyRegistry) vc.present(bankRechargeNavigationController, animated: true) // Waiting for animation completion DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { self.FBSnapshotVerifyLayer(self.window.layer) exp.fulfill() }) wait(for: [exp], timeout: 10) } private final class ViewController: UIViewController { override var prefersStatusBarHidden: Bool { return true } } }
capture_snapshots lane
fastlane
を使用してスナップショット画像を作成するために capture_snapshots
というlaneを作成しました。
private_lane :snapshot_devices do [ "iPad Pro (11-inch) (13.1)", "iPhone 8 (13.1)", "iPhone SE (12.2)", "iPhone Xs (12.2)", "iPhone Xs Max (12.2)", "iPhone Xʀ (12.2)" ] end lane :capture_snapshots do xcversion(version: "11.1") sh("rm", "-fr", "../Snapshots/SnapshotsTests/ReferenceImages_64/") ENV['SNAPSHOT_TEST_RECORD_MODE'] = 'true' scheme = ENV['SNAPSHOT_TEST_SCHEME'] snapshot_devices.each do |device| scan( scheme: scheme, device: device, fail_build: false ) end end
スナップショットテスト用の画像は Snapshots/SnapshotsTests/ReferenceImages_64/
ディレクトリに出力されます。
スナップショットテスト
snapshot_test lane
fastlane
を使用してCI上でスナップショットテストを実行するために snapshot_test
というlaneを作成しました。
lane :snapshot_test do scheme = ENV['SNAPSHOT_TEST_SCHEME'] test_scheme( scheme: scheme, devices: snapshot_devices ) end
テストが失敗すると、 Snapshots/SnapshotsTests/FailureDiffs/
というディレクトリに失敗した画像が出力されます。
以下の失敗時のサンプル画像です。
CircleCIのartifactsに失敗した画像を保存
CircleCIのjobの設定で、スナップショットテストに失敗した画像をartifactsに保存するようにしています。
そうすること、Web上からスナップショットテストが失敗した際の画像の確認ができ、修正作業を行いやすいようにしています。
- store_artifacts: path: Snapshots/SnapshotsTests/FailureDiffs
CircleCIのWorkflow設定
merpay-ios-sdk
では、CircleCI上で unit_test
と snapshot_test
を並列に実行することで、テスト時間を削減する工夫もしています。
メルペイではミッション・バリューに共感できるiOSエンジニアを募集しています。一緒に働ける仲間をお待ちしております。
apply.workable.com
明日のMerpay Advent Calendar 執筆担当は、 Experts Team の @tenntenn さんです。引き続きお楽しみください。
p.s.
アドベントカレンダーには参加していませんが、先週、メルペイ iOSチームの @kenmaz さんがブログを公開しました。
Xcode Previewsを用いたUIKitベースのプロジェクトの開発方法の詳細と、それによって得られたメリットや知見について紹介しています。宜しければこちらもご覧ください。