Slide 1

Slide 1 text

スナップショットテスト 実戦投⼊ iOSDC Japan 2019 株式会社Diverse 今泉智博 (@imaizume) A1

Slide 2

Slide 2 text

A2 ⾃分のアプリにスナップショットテストを導⼊する (今後より多くのスナップショットテストに関する知⾒が発信される) ⽬標 このトークを聞いてもらいたい⽅ • アプリの表⽰確認作業で苦労したことがある • 予期せぬ表⽰崩れや表⽰バグに悩まされたことがある • スナップショットテストについて知らない/知りたい 以下のいずれかに当てはまる⽅✋

Slide 3

Slide 3 text

A3 おことわり • 今回は時間の都合上、UIのスナップショット に絞ってお話します • 表⽰内容は全てテストデータです • 本資料はSpeakerDeckにて公開済みです ⼀部⾒えにくい箇所等はお⼿元でご覧下さい • 「スナップショットテスト」は、より⼀般的 には「画像ベーステスト」や、「ビジュアル リグレッションテスト」と呼ばれています。

Slide 4

Slide 4 text

A4 本⽇のindex • @imaizumeとPoiboyの紹介 • スナップショットテスト導⼊の背景 • 初期設定 • 撮影時の表⽰条件・状態整理 • 状態再現のための設計修正 • 撮影した情報の整理 • 様々なViewの撮影 • TIPS&ハマりポイント • まとめ

Slide 5

Slide 5 text

A5 ⾃⼰紹介 Tomohiro Imaizumi - @imaizume Androidユーザー歴8年のiOS開発者 (iOS歴: 2年半)            / ♨ 銭湯巡りが趣味 (お遍路88件達成)

Slide 6

Slide 6 text

A6 ⼥性から選ぶ マッチングアプリ 運営4年⽬ / 110万DL突破! Poiboyの紹介 https://poiboy.jp

Slide 7

Slide 7 text

A7 Diverseブースにもぜひ遊びに来てください! マッチングアプリを⾝近に感じてもらえる コンテンツを⽤意して待っています! 緑の看板が⽬印

Slide 8

Slide 8 text

A8 本⽇のindex • @imaizumeとDiverseの紹介 • スナップショットテスト導⼊の背景 • 初期設定 • 撮影時の表⽰条件・状態整理 • 状態再現のための設計修正 • 撮影した情報の整理 • 様々なViewの撮影 • TIPS&ハマりポイント • まとめ

Slide 9

Slide 9 text

A9 みなさんに聞きたいこと 正直ツラくないですか リリース前のアプリの表⽰確認って

Slide 10

Slide 10 text

A10 表⽰確認での悩み1: 条件網羅が難しい • 特定条件下のみで起こる不具合 • レスポンス / 内部状態 / 機種 など条件が複雑 • 全条件で⼿動確認するのはコストが⼤きい

Slide 11

Slide 11 text

A11 表⽰確認での悩み2: 時間がかかる • 状態再現に時間がかかる • アーカイブ&beta版配信にも時間がかかる

Slide 12

Slide 12 text

A12 表⽰確認での悩み3: 予期せぬ画⾯崩壊 • Auto Layoutの制約漏れに気づかない • ある画⾯のViewや状態変更が別画⾯にも影響

Slide 13

Slide 13 text

A13 ؒٝآص، ذأة٦ • 㹋鄲 • رغحؚ • ذأز • ٖؽُ٦ ،٦ؕ؎ـ ⹛⡲然钠 غؚ㜠デ • ⥜姻 ،٦ؕ؎ـ ⹛⡲然钠 غؚ㜠デ • 鷄⸇⥜姻 ،٦ؕ؎ـ ⹛⡲然钠 ⼿動での表⽰確認時 数⼗分待ち 全パターン ⾒きれない 別画⾯が デグレ エッジケース でバグ発⾒ 表⽰確認がツラい

Slide 14

Slide 14 text

ؒٝآص، ذأة٦ • 㹋鄲 • رغحؚ • ذأز • ٖؽُ٦ ،٦ؕ؎ـ ⹛⡲然钠 غؚ㜠デ • ⥜姻 ،٦ؕ؎ـ ⹛⡲然钠 غؚ㜠デ • 鷄⸇⥜姻 ،٦ؕ؎ـ ⹛⡲然钠 現状: 表⽰確認がつらい 数⼗分待ち 全パターン ⾒きれない 別画⾯が デグレ エッジケース でバグ発⾒ この状況を打破するため 新たな武器が必要 A14

Slide 15

Slide 15 text

ؒٝآص، ذأة٦ • 㹋鄲 • رغحؚ • ذأز • ٖؽُ٦ ،٦ؕ؎ـ ⹛⡲然钠 غؚ㜠デ • ⥜姻 ،٦ؕ؎ـ ⹛⡲然钠 غؚ㜠デ • 鷄⸇⥜姻 ،٦ؕ؎ـ ⹛⡲然钠 現状 数⼗分待ち 全パターン ⾒きれない 別画⾯が デグレ エッジケース でバグ発⾒ 今こそ スナップショットテスト 実戦投⼊の時 A15

Slide 16

Slide 16 text

A16 スナップショットテストとは? ⾃動でアプリの内部状態(UI)を記録(撮影)し 正しい状態との⽐較・差分検出をする仕組み AFTER BEFORE AFTER BEFORE

Slide 17

Slide 17 text

A17 指定したUIの状態を再現し⼀括撮影 • テスト実⾏で指定した画⾯やViewを⾃動撮影 • 複数の表⽰パターンも⼀括で撮影 ⼿動での状態再現と撮影が不要に/ ⾃動で撮影されたスクリーンショットの例

Slide 18

Slide 18 text

A18 スナップショットテスト実⾏時の様⼦

Slide 19

Slide 19 text

A19 過去の画像と⽐較し差分を検出 • 対応する過去のUI状態と⽐較 • Viewの配置や⾊の差分を検出 • 差分を画像出⼒しテストをFailさせる 表⽰のデグレを⾃動で検出可能/ 撮影 ⽐較 通知 元のレイアウト 差分 撮影した画像

Slide 20

Slide 20 text

A20 ؒٝآص، ذأة٦ • 㹋鄲 • رغحؚ • ذأز • ٖؽُ٦ ،٦ؕ؎ـ ⹛⡲然钠 غؚ㜠デ • ⥜姻 ،٦ؕ؎ـ ⹛⡲然钠 غؚ㜠デ • 鷄⸇⥜姻 ،٦ؕ؎ـ ⹛⡲然钠 全表⽰パターンとデグレ確認✅ 修正確認の ループを減らせる 表⽰確認がツラくない スナップショットテスト導⼊後 スナップショット テスト実⾏

Slide 21

Slide 21 text

A21 スナップショットテストが動作するレイヤー 全レイヤーで実⾏可能 (基本はUIより下) 6* 窟さ ⽃⡤ 䩛⹛ذأز テストピラミッドと スナップショットテスト Viewの撮影/テスト ViewControllerの撮影/テスト テストの⽬的: 「ある条件下の表⽰の正しさ確認」

Slide 22

Slide 22 text

A22 スナップショットテストのメリット 基本は単体/統合テスト上で実⾏ • UI操作での画⾯到達の必要なし • UIテストと⽐較して⾼速に動作 ログインをモックせずに目的の画面を撮影可能

Slide 23

Slide 23 text

A23 スナップショットテストのためのライブラリ • Facebookが作成したFBSnapshotTestCaseをuberがメンテ • UIViewまたはCALayerの画像を撮影 iOSSnapshotTestCase (FBSnapshotTestCase) SnapshotTesting • View階層やURLRequest等の多様なスナップショットが可能 • 完全Swift製ライブラリ 以降はiOSSnapshotTestCaseを例に説明します github.com/uber/ios-snapshot-test-case github.com/pointfreeco/swift-snapshot-testing

Slide 24

Slide 24 text

A24 本⽇のindex • @imaizumeとDiverseの紹介 • スナップショットテスト導⼊の背景 • 初期設定 • 撮影時の表⽰条件・状態整理 • 状態再現のための設計修正 • 撮影した情報の整理 • 様々なViewの撮影 • TIPS&ハマりポイント • まとめ

Slide 25

Slide 25 text

A25 ライブラリの導⼊ • CocoaPods / Carthageでインストール • 環境変数を設定 湡涸 㢌侧せ 鏣㹀⦼ 嫰鯰欽歗⫷ ך⳿⸂⯓ '#@3&'&3&/$&@*."(&@%*3 4063$&@3005 130+&$5@/".& 5FTUT3FGFSFODF*NBHFT 䊴ⴓ歗⫷ ך⳿⸂⯓ *."(&@%*''@%*3 4063$&@3005 130+&$5@/".& 5FTUT'BJMVSF%JGGT target 'YourAppTests' do inherit! :search_paths pod "iOSSnapshotTestCase" end Podfile github "uber/ios-snapshot-test-case" Cartfile (Swift Package Managerは現在対応中) • 本発表での実⾏環境 Swift 4.2 XCode 10.3 (10G8)

Slide 26

Slide 26 text

A26 実⾏の流れ(全体) 撮影と差分検出の2パートに分かれる false recordMode 嫰鯰㼎韋 ך歗⫷ true 嫰鯰㼎韋 ך歗⫷ 䊴ⴓ ז׃ ֮׶ ז׃ ֮׶ ♳剅ֹ乆䕦 倜鋉乆䕦 ז׃ ֮׶ recordModeⴖ׶剏ִ

Slide 27

Slide 27 text

A27 前半: スクリーンショットの撮影 false recordMode 嫰鯰㼎韋 ך歗⫷ true 嫰鯰㼎韋 ך歗⫷ 䊴ⴓ ז׃ ֮׶ ז׃ ֮׶ ♳剅ֹ乆䕦 倜鋉乆䕦 ז׃ ֮׶ recordModeⴖ׶剏ִ

Slide 28

Slide 28 text

A28 テストファイル作成とrecordModeの設定 • FBSnapshotTestCaseを継承したテストクラスを作成 • 撮影時だけ recordMode = true に import FBSnapshotTestCase @testable import YourApp class MySnapshotTestCase: FBSnapshotTestCase { override func setUp() { super.setUp() self.recordMode = true } ... } SampleSnapshotTests.swift デフォルトでfalseなので撮影後はコメントアウト

Slide 29

Slide 29 text

A29 スナップショットの撮影 • 撮影したいViewを⽣成 • FBSnapshotVerifyView メソッドを呼び出す • identifier に画像名を指定可能 class MySnapshotTestCase: FBSnapshotTestCase { ... func testSimpleView() { let view = UIView( frame: CGRect(x: 0, y: 0, width: 64, height: 64)) view.backgroundColor = .blue FBSnapshotVerifyView(view, identifier: "simple_view_snapshot") } } SampleSnapshotTests.swift

Slide 30

Slide 30 text

A30 実⾏結果 "failed - Test ran in record mode. Reference image is now saved." と出れば成功 • (YourAppTests)/ReferenceImages_64以下に 撮影された画像(参考画像)が⽣成

Slide 31

Slide 31 text

A31 後半: 差分の検出 false recordMode 嫰鯰㼎韋 ך歗⫷ true 嫰鯰㼎韋 ך歗⫷ 䊴ⴓ ז׃ ֮׶ ז׃ ֮׶ ♳剅ֹ乆䕦 倜鋉乆䕦 ז׃ ֮׶ recordModeⴖ׶剏ִ

Slide 32

Slide 32 text

A32 コードを変えずにテストを実⾏ class MySnapshotTestCase: FBSnapshotTestCase { override func setUp() { super.setUp() // self.recordMode = true } func testSimpleView() { let view = UIView( frame: CGRect(x: 0, y: 0, width: 64, height: 64)) view.backgroundColor = .blue FBSnapshotVerifyView(view, identifier: "simple_view_snapshot") } } SampleSnapshotTests.swift recordMode = falseで再実⾏ → テストが通る

Slide 33

Slide 33 text

A33 UILabelを加えてテストを実⾏ func testSimpleView() { let view = UIView( frame: CGRect(x: 0, y: 0, width: 64, height: 64)) view.backgroundColor = .blue let label = UILabel( frame: CGRect(x: 0, y: 16, width: 64, height: 32)) label.text = "Snapshot!!" label.textColor = .white view.addSubview(label) FBSnapshotVerifyView(view, identifier: "simple_view_snapshot") } SampleSnapshotTests.swift テストが失敗しFailureDiffsに差分画像が出現 差分 実⾏時 ⽐較元

Slide 34

Slide 34 text

A34 背景⾊を変えてテストを実⾏ func testSimpleView() { let view = UIView( frame: CGRect(x: 0, y: 0, width: 64, height: 64)) view.backgroundColor = .red FBSnapshotVerifyView(view) } SampleSnapshotTests.swift テストが失敗しFailureDiffsに差分画像が出現 (この場合は判りにくいですが、⾊の差でもdiffが出⼒されます。) 差分 実⾏時 ⽐較元

Slide 35

Slide 35 text

A35 再掲: 実⾏の流れ(全体) 基本は「撮影→差分⽐較」の繰り返し false recordMode 嫰鯰㼎韋 ך歗⫷ true 嫰鯰㼎韋 ך歗⫷ 䊴ⴓ ז׃ ֮׶ ז׃ ֮׶ ♳剅ֹ乆䕦 倜鋉乆䕦 ז׃ ֮׶ recordModeⴖ׶剏ִ

Slide 36

Slide 36 text

A36 本⽇のindex • @imaizumeとDiverseの紹介 • スナップショットテスト導⼊の背景 • 初期設定 • 撮影時の表⽰条件・状態整理 • 状態再現のための設計修正 • 撮影した情報の整理 • 様々なViewの撮影 • TIPS&ハマりポイント • まとめ

Slide 37

Slide 37 text

؟ٝفٕ 植㹋ך،فٔ 邌爙勴⟝ א 醱侧 7JFXך欰䧭 ꫼涸 ⹛涸ז皘䨽֮׶ Ⰵ⸂ ♶銲 "1*ⰻ鿇朐䡾ח⣛㶷 A37 サンプルと現実のアプリ 必要なこと 1. 撮影時の表⽰条件・状態整理 2. 状態再現のための設計修正 撮影までに超えねばならない壁が存在

Slide 38

Slide 38 text

A38 撮影したい状態の確認 撮影したい表⽰状態が複数ある場合 どの状態で撮影するかを整理しておく 課⾦状態 ログイン状態 プロフィール 写真 親画⾯ (どの状態を再現するんだっけ?)

Slide 39

Slide 39 text

A39 例: Poiboyのプロフィール画⾯ • 相⼿または⾃分のプロフィールを表⽰ • 表⽰項⽬が複数で表⽰条件が複雑 プロフィール画⾯の例

Slide 40

Slide 40 text

A40 例: Poiboyのプロフィール画⾯ • 相⼿または⾃分のプロフィールを表⽰ • 表⽰項⽬が複数で表⽰条件が複雑 プロフィール画⾯の例 年齢 / 住所/ 職業 名前の⽂字数 プロフ写真数 プロフの⻑さ 共通点の数・内容 ボタンの表⽰・テキスト ページャー (画⾯サイズ) (OSバージョン) タイトルテキスト (性別で変化) オンライン状態

Slide 41

Slide 41 text

A41 表⽰条件と項⽬を整理 プロフィール画⾯でスナップショット撮影する時の状態のリスト どの表⽰項⽬を撮影すべきか・必要条件が何か 仕様書やテスターと確認する 条件の漏れ/不要に気づくためにも必要

Slide 42

Slide 42 text

A42 表⽰条件と項⽬を整理 条件の漏れ/不要に気づくためにも必要 プロフィール画⾯でスナップショット撮影する時の状態のリスト どの表⽰項⽬を撮影すべきか・必要条件が何か 仕様書やテスターと確認する 職業: ⽂字数の境界値で 名前: 境界値+0⽂字 (全⾓で) 共通点: 0~6個のケース全て⾒たい プロフ⽂: スクロールの有無を確認 都道府県: 考慮不要

Slide 43

Slide 43 text

A43 参考: ⼿動表⽰確認はテスターもツラい テスター⽤のプロフィール画⾯の確認項⽬リストの例

Slide 44

Slide 44 text

A44 本⽇のindex • @imaizumeとDiverseの紹介 • スナップショットテスト導⼊の背景 • 初期設定 • 撮影時の表⽰条件・状態整理 • 状態再現のための設計修正 • 撮影した情報の整理 • 様々なViewの撮影 • TIPS&ハマりポイント • まとめ

Slide 45

Slide 45 text

؟ٝفٕ 植㹋ך،فٔ 邌爙勴⟝ א 醱侧 7JFXך欰䧭 ꫼涸 ⹛涸ז皘䨽֮׶ Ⰵ⸂ ♶銲 "1*ⰻ鿇朐䡾ח⣛㶷 A45 再掲: サンプルと現実のアプリ 撮影までに超えねばならない壁 必要なこと 1. 撮影時の表⽰条件・状態整理 2. 状態再現のための設計修正

Slide 46

Slide 46 text

A46 状態再現が易しいケース (Viewに値を渡すだけで表⽰が再現できるような場合) ⼊⼒が初期化パラメータのみに依存する場合

Slide 47

Slide 47 text

A47 例: ブースト完了ポップアップ • ブースト(アイテム使⽤)状態の完了を通知する • ブースト中に異性にプロフィールが露出した回数を表⽰ 「完了」だけでなく「成功」のパターンも確認したい 桁が多いケースでの表⽰確認をしたい • 「完了」「成功」のフラグ • スコア: 1~4桁の数値 画⾯への⼊⼒

Slide 48

Slide 48 text

A48 パラメーター化テストにする func testBoostComplete() { self.folderName = "ブースト完了ポップアップ" let testCases: [(dependency: Dependency, identifier: String)] = [ (.init(rateValue: 0.1, poied: false), "ブーストスコア01"), (.init(rateValue: 1.2, poied: false), "ブーストスコア12"), (.init(rateValue: 12.3, poied: false), "ブーストスコア123"), (.init(rateValue: 123.4, poied: false), "ブーストスコア1234"), (.init(rateValue: 1.2, poied: true), "ブーストポイされた"), ] testCases.forEach { testCase in let vc: BoostCompleteViewController = .init(with: testCase.dependency) FBSnapshotVerifyView(vc.view, identifier: testCase.identifier) } } SampleSnapshotTests.swift 保存先のフォルダ名を指定可能 依存先が1つでかつパラメータ数も少ないため簡単 (ViewControllerへの依存注⼊に github.com/tarunon/Instantiate を利⽤) 異なるidentifierならループで回せる

Slide 49

Slide 49 text

A49 実際に撮影された画像 複数条件でスクリーンショットが撮れた! ※表⽰の都合上切り抜いていますが実際はフルスクリーンでキャプチャされています。 ⾒切れているので修正が必要 成功 完了/2桁 完了/3桁 完了/4桁

Slide 50

Slide 50 text

A50 状態再現が難しいケース ⼊⼒が複数モジュール・環境に依存する場合 • 初期化パラメーター • 画⾯サイズ • APIレスポンス • グローバル変数 差し替え可能にする必要

Slide 51

Slide 51 text

A51 例: プロフィール画⾯ • APIレスポンス • 名前 • ログイン状態 • 共通点数 • etc ... • グローバル変数 • 性別 • A/Bテストの振り分け • 初期化パラメータ • 親画⾯の種別 • 画⾯サイズ 画⾯への⼊⼒

Slide 52

Slide 52 text

A52 ⼊⼒を差し替え可能にするために • 基本的にMVPだが⼀部画⾯がFat ViewController • 依存注⼊によるモジュールの差し込みが不可 • グローバル変数の参照箇所あり アプリの設計修正が必要 (現状)

Slide 53

Slide 53 text

A53 Fat ViewControllerを分解 7JFX 1SFTFOUFS .PEFM 4FSWJDF ذأز؝٦س 'BU7JFX 4FSWJDF ذأز؝٦س "1*

Slide 54

Slide 54 text

A54 依存注⼊を可能に 7JFX 1SFTFOUFS .PEFM 4FSWJDF ذأز؝٦س "1* 7JFXⰻדJOJU 1SFTFOUFSⰻדJOJU .PEFMⰻדJOJU 7JFX 1SFTFOUFS ذأز؝٦س 1SFTFOUFS4UVC

Slide 55

Slide 55 text

A55 Serviceでもモックする場合 7JFX 1SFTFOUFS .PEFM 4FSWJDF ذأز؝٦س "1* 7JFXⰻדJOJU 1SFTFOUFSⰻדJOJU .PEFMⰻדJOJU 7JFX 1SFTFOUFS .PEFM 4FSWJDF ذأز؝٦س 4FSWJDF4UVC ٗ٦ٕؕך +40/ؿ؋؎ٕ 1SFTFOUFS4UVC

Slide 56

Slide 56 text

7JFX 1SFTFOUFS .PEFM 4FSWJDF ؚٗ٦غٕ 㢌侧 ذأز؝٦س "1* ؒٝس ه؎ٝز 䚍ⴽ "#㾩䚍 7JFX 1SFTFOUFS .PEFM 4FSWJDF ؚٗ٦غٕ 㢌侧 ذأز؝٦س "1* 4UPSF QSPUPDPM 4UVC A56 明⽰的⼊⼒と抽象へ依存する構造に 明⽰的な⼊⼒ 抽象への依存 暗黙的⼊⼒ 具体への依存 「iOSアプリのテストを書きたいのに書けないあなたへ」より

Slide 57

Slide 57 text

A57 例: 性別をリポジトリパターンに protocol GenderStoreContract { var isFemale: Bool { get } var isMale: Bool { get } } class GenderStore: GenderStoreContract { var isFemale: Bool { return KeychainManager.shared.isFemale() } var isMale: Bool { return KeychainManager.shared.isMale() } } class GenderStoreStub: GenderStoreContract { let isFemale: Bool let isMale: Bool init(isFemale: Bool) { self.isFemale = isFemale self.isMale = !isFemale } } GenderStore.swift 「iOSアプリのテストを書きたいのに書けないあなたへ」より 任意の性別に差し替える アプリ内フラグを参照する

Slide 58

Slide 58 text

A58 例: Presenterに性別を依存⼊⼒ class ProfilePresenter: NSObject { init(_ gender: GenderStoreContract, _ dataSource: ProfileDataSource) { self.gender = gender self.dataSource = dataSource // データソース super.init() self.dataSource.output = self } } let stub: GenderStoreStub = .init(isFemale: true) // 性別=女性 let presenter: ProfilePresenter = .init(stub, dataSource) ProfilePresenter.swift protocol = 抽象へ依存 任意の性別を依存注⼊

Slide 59

Slide 59 text

A59 例: Serviceのメソッドの抽象化 protocol ProfileServiceInput { /// 指定した会員のプロフィールを取得するPromiseオブジェクトを返す /// /// - parameters: /// - targetId: 取得する会員のメンバーID func fetchMemberLookupRequest(for targetId: Int) -> Promise } /// APIを叩いてデータを取得するService class ProfileService: ProfileServiceInput { func fetchMemberLookupRequest(for targetId: Int) -> Promise { return APIManager.shared.getPromise( .memberLookup(targetId), apiTargetStore: self.apiTarget) } } SampleSnapshotTests.swift

Slide 60

Slide 60 text

A60 例: JSONファイルからレスポンスを返す import ObjectMapper /// ローカルのJSONからデータを取得するService class ProfileServiceStub: ProfileServiceInput { private let json: [String: Any] init(_ json: [String: Any]) { self.json = json super.init() } // Snapshot Test時のみ実行 func fetchMemberLookupRequest(for targetId: Int) { let model = ProfileResponseModel(JSON: self.json)! return Promise { resolver in resolver.fulfill(model) } } } SampleSnapshotTests.swift (ORマッピングには tristanhimmelman/ObjectMapper を利⽤)

Slide 61

Slide 61 text

// テストパラメーターから必要な状態のViewを生成して返す private func createView(_ json: [String: Any], _ input: ProfilePresenterInput) -> ProfileViewController { let serviceStub: ProfileServiceStub = .init(json) let dataSource: ProfileDataSource = .init(serviceStub) let presenterInput: ProfilePresenterInput = .init(...) let presenter: ProfilePresenter = .init(input, dataSource) return ProfileViewController(with: .init(presenter: presenter)).view } // テストパラメーターをタプルにまとめて let testCases: [(json: [String: Any], input: (origin: OriginType, gender: GenderStoreStub), identifier: String)] = [ (JSONFiles.Profile1.result, (.talk, .female), "女性がトーク画面から遷移"), (JSONFiles.Profile2.result, (.appeal, .male), "男性がアピール画面から遷移"), ... ] // ループで回して撮影 testCases.forEach { testCase in let view = self.createView(testCase.result, input) FBSnapshotVerifyView(view, identifier: testCase.identifier) } パラメータと共通処理をまとめる A61 例: 依存注⼊で画⾯を⽣成し撮影

Slide 62

Slide 62 text

A62 撮影に成功 プロフィール基本表⽰

Slide 63

Slide 63 text

A63 撮影に成功 ボタンの表⽰とテキスト 共通点とページャーの表⽰

Slide 64

Slide 64 text

A64 • ViewControllerの場合シミュレーターのサイズで撮 影 (iOSSnapshotTestCaseの場合) • ローカル実⾏ではデバイス切り替えの⼿間がやや⾯倒 撮影時の画⾯サイズ・OSバージョンの変更 FolioさんのFastfileでパラメータ化しテスト実⾏ github.com/folio-sec/Fastfile より func snapshot_test(workspace, scheme, device, os_version, only_testing) { system("xcodebuild test-without-building -workspace #{workspace} -scheme #{scheme} RECORD_MODE_ENV=true -destination 'name=#{device},OS=#{os_version}' -only-testing:#{only_testing}") } snapshot_testのlane定義

Slide 65

Slide 65 text

A65 依存注⼊ VS 環境変数/プリプロセッサマクロ 갪湡 ⣛㶷岣Ⰵ 橆㞮㢌侧 鏣鎘⥜姻 䗳銲זֿהָ֮׷ קר♶銲 ذأزٗآحؙך 㹀纏㜥䨽 فٗتؙءّٝ הכ殯ז׷㜥䨽 فٗتؙءّٝ הずׄ㜥䨽 ٖ؎َ٦׀הך 䊴׃剏ִ 〳腉 ꨇ׃ְ 柔軟にテストを書いていくなら ⼿間はかかるが依存注⼊がおすすめ #if TEST // テスト時のみ実行 #endif プリプロセッサマクロで分岐する例 if ProcessInfo().environment["TEST"] != nil { /*テスト時のみ実行*/ } 環境変数で分岐する例

Slide 66

Slide 66 text

A66 本⽇のindex • @imaizumeとDiverseの紹介 • スナップショットテスト導⼊の背景 • 初期設定 • 撮影時の表⽰条件・状態整理 • 状態再現のための設計修正 • 撮影した情報の整理 • 様々なViewの撮影 • TIPS&ハマりポイント • まとめ

Slide 67

Slide 67 text

A67 デフォルトのフォルダ/画像名は分かりづらい • そのままでは各画像が何を表現しているか分かりづらい • folderNameとidentifierを適切に設定してあげる必要 デフォルトのフォルダ・画像名の例

Slide 68

Slide 68 text

A68 フォルダ・ファイル名の命名規則 • 例えば • {folderName}に期待する状態 • {identifier}にコンテキストを与える • 状態が特に無ければ役割やカテゴリでグループ化 ㄏせ鋉⵱ デフォルト {Scheme}.{Testクラス} (例: YourAppTest.MySnapshotTests) 指定時 {folderName} (例: プロフ基本表示) デフォルト {メソッド}_{fileNameOptions}.png (例: testView_iPhone_12_4.png) 指定時 {メソッド}_{identifier}_{fileNameOptions}.png (例: testView_スクロール後_iPhone_12_4.png) 갪湡 フォルダ 画像 fileNameOptionsは 次ページで解説

Slide 69

Slide 69 text

A69 fileNameOptionsについて • ファイル名に出⼒するテスト時の環境情報 • 表⽰確認時に必要な環境情報はここで指定しておく self.fileNameOptions = [.device, .OS, .screenSize, .screenScale] fileNameOptionsの指定例 画像名: testRecoveryPopup_残りアピール数1個[email protected] ؔفءّٝ 嚊銲 ⦼ך《䖤⯋ ⢽ .none ז׃ - - .device رغ؎أ䞔㜠 UIDevice.current.model iPhone, iPad .OS 04غ٦آّٝ UIDevice.current.systemVersion 11_3, 12_4 .screenSize 歗꬗؟؎ؤ UIScreen.main.bounds.size 375x812 .screenScale ⦓桦 UIScreen.main.scale @2x, @3x

Slide 70

Slide 70 text

A70 フォルダ/画像名の指定 期待する状態とコンテキストで画像を整理した例 確認すべき項⽬と条件の把握が容易に self.folderName = "詳細プロフでボタンが非表示になる" FBSnapshotVerifyView(vc.view, identifier: "トーク画面から遷移した時") ... folderNameとidentifierを設定後

Slide 71

Slide 71 text

A71 • ⽬的の画像を探しプレビューするのが⼿間 • テスターやデザイナーと話すにも不便 • レポートに出⼒したい... が、公式には機能がない 今度は画像が⼤量⽣成されて分かりづらい ⼤量に出⼒されたスクリーンショットの例

Slide 72

Slide 72 text

A72 ⾃前でレポートへ出⼒するスクリプトを作成 https://gist.github.com/imaizume/6aa2537c1c6778b50873c813eb7a15f1 Folioさんの⼒を再び借りる → screenshots-preview-generator.rb • 画像⽣成後に実⾏→Markdownでレポートを出⼒ • 場合により⽂字列マッチのパターンを変える必要 • ファイル・フォルダの命名規則に応じて • 出⼒先やフォーマットを変えたい時 ※上記を参考に @imaizume の作成したスクリプトも公開中 https://raw.githubusercontent.com/folio-sec/Fastfile/master/Scripts/screenshots-preview-generator.rb

Slide 73

Slide 73 text

A73 出⼒されるレポートの例 OSバージョン 画⾯サイズ identifier folderName

Slide 74

Slide 74 text

A74 参考画像とレポートをGitHub上へUP レポートを誰でも簡単に閲覧可能

Slide 75

Slide 75 text

A75 条件網羅が難しい問題 条件整理後にパラメーター化テストへ 複数状態をループで撮影 時間がかかる問題 アーカイブや状態再現のための時間が不要に 予期せぬ画⾯崩壊 1度撮影しておけば⾃動テスト実⾏時に通知 スナップショットテスト導⼊による成果

Slide 76

Slide 76 text

条件網羅が難しい問題 条件整理後にパラメーター化テストへ 複数状態をループで撮影 時間がかかる問題 アーカイブや状態再現のための時間が不要に 予期せぬ画⾯崩壊 1度撮影しておけば⾃動テスト実⾏時に通知 スナップショットテストによる成果 しかし実戦投⼊では さらなるハマりポイントが A76

Slide 77

Slide 77 text

A77 本⽇のindex • @imaizumeとDiverseの紹介 • スナップショットテスト導⼊の背景 • 初期設定 • 撮影時の表⽰条件・状態整理 • 状態再現のための設計修正 • 撮影した情報の整理 • 様々なViewの撮影 • TIPS&ハマりポイント • まとめ

Slide 78

Slide 78 text

A78 ViewController撮影時のサイズについて class FromXibVC: UIViewController { init() { super.init(nibName: "FromXibVC", bundle: .main) } } let vc = FromXibVC() Xibから⽣成した画⾯ let sb = UIStoryboard( name: "FromStoryboardVC", bundle: .main) let vc = sb .instantiateInitialViewController() as! FromStoryboardVC Storyboardから⽣成した画⾯ ⽣成⽅法の異なる2つのViewControllerを ①Storyboardから生成 ②Xibから生成 Xib/Storyboardのプレビュー: iPhone SE シミュレータ: iPhone SE / 6s / Xs ... 同じシミュレーター環境で撮影

Slide 79

Slide 79 text

A79 撮影結果 ①Storyboardから生成 ②Xibから生成 Xib⽣成の場合は Interface Builderのプレビューサイズで撮影される 全て同じサイズになる

Slide 80

Slide 80 text

A80 対策: Root Viewのサイズを指定する class ChanceTimePopupView: UIView { ... } class PopupSnapshotTest: FBSnapshotTestCase { ... func testMalePopups() { let vc: ChanceTimePopupViewController = .init() // Root Viewのサイズを指定 vc.view.frame = UIScreen.main.bounds FBSnapshotVerifyView(vc.view, identifier: "5つ星") } } Xibから作ったViewControllerのサイズを指定する 画⾯サイズが変化しない時は ViewControllerの⽣成⽅法を確認

Slide 81

Slide 81 text

class ProfileViewController: UIViewController { override func viewDidLoad() { // 非同期の通信 self.service.callApi( params: params, success: { model in // メインスレッドでのUIの更新 DispatchQueue.main.async { self.imageView?.image = model.isCampaigning ? Asset.campaignImage.image : Asset.defaultImage.image } }, failure: { res, error in print(error) }) ...} ⾮同期処理が発⽣する画⾯ A81 ⾮同期実⾏が必要な場合 • DispatchQueueによる処理 • 実際のAPI通信を利⽤したデータ取得時 データ取得が遅れ撮影時に反映されない

Slide 82

Slide 82 text

A82 対策: XCTExpectationで⾮同期にテストを実⾏ func testFemaleProfileRegister() { let vc = MyAsyncViewController() self.setupView(vc: vc) let exp = self.expectation(description: "UI描画") if XCTWaiter.wait(for: [exp], timeout: 1.0) == .timedOut { FBSnapshotVerifyView(vc.view, identifier: "非同期処理") } } ⾮同期処理を1.0秒待ってから撮影するテストの例 • ⾮同期処理完了でXCExpectation#fulfillを呼ぶ • またはXCTWaiter#waitで適当な時間待ってから撮影 データが正常に表⽰されないときは ⾮同期処理が⾛っていないかを確認

Slide 83

Slide 83 text

A83 Navigation Barを含めての画⾯撮影 let nc: UINavigationController = .init(rootViewController: viewController) nc.title = "プロフィール登録" let window: UIWindow = .init(frame: nc.view.bounds) window.addSubview(nc.view) FBSnapshotVerifyView(nc.view, identifier: "ナビゲーション") Navigation Barを表⽰させる UIWindow.addSubviewしない場合 UIWindow.addSubviewした場合 UIWindowのsubViewに追加する必要あり

Slide 84

Slide 84 text

class CustomCellSnapshotTests: UITableViewDelegate, UITableViewDataSource { func testCustomCell() { let tableView: UITableView = .init(of: sizeOfTable) tableView.register(nib, forCellReuseIdentifier identifier) tableView.delegate = self tableView.dataSource = self tableView.reloadData() FBSnapshotVerifyView(self.tableView, identifier: identifier) } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {...} func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 1 } } UITableViewCellの撮影 A84 UITable(Collection)ViewCellの撮影 • Cellは単独で初期化できない • CellのコンテンツをカスタムViewに • またはテストクラスにDelegate/DataSourceを定義

Slide 85

Slide 85 text

A85 本⽇のindex • @imaizumeとDiverseの紹介 • スナップショットテスト導⼊の背景 • 初期設定 • 撮影時の表⽰条件・状態整理 • 状態再現のための設計修正 • 撮影した情報の整理 • 様々なViewの撮影 • TIPS&ハマりポイント • まとめ

Slide 86

Slide 86 text

A86 状態再現⽤のAPIレスポンスを⽤意する⽅法 • レスポンスのモックにOpenAPIやApiaryを使えたら理想 • しかし現状のPoiboyではAPI定義をWikiに書く運⽤ • そこで通信をCharles Proxyでキャプチャ • スタブ⽤JSONを作成し適宜バリエーションを増やす

Slide 87

Slide 87 text

A87 viewWill/DidAppearが呼ばれない override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.presenter.setupViews() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.presenter.setupViewsAgain() } viewWill/DidAppearでpresenterをコールする例 上記のような場合 presenterのメソッド が呼ばれず空のViewが撮影される (iOSSnapshotTestCaseの場合)

Slide 88

Slide 88 text

A88 ライフサイクルメソッドのコールを確認 class MyViewController: UIViewController { override func loadView() { super.loadView() print("Called loadView") } ... // 以下同様にviewDidDisappearまでコールを確認する } SampleSnapshotTests.swift Called loadView Called viewDidLoad Called viewWillLayoutSubview Called viewDidLayoutSubview Output オブジェクトは⽣成されるが階層に追加されないため viewWill/DidAppearが呼ばれない

Slide 89

Slide 89 text

class ProfileViewController: UIViewController { ... /// 正しい表示を再現するためのヘルパーメソッド private func setupView(vc: T) -> T { // 他の設定も必要に応じて vc.modalTransitionStyle = .crossDissolve vc.modalPresentationStyle = .overCurrentContext // viewWillAppearの呼び出し vc.beginAppearanceTransition(true, animated: false) // viewDidAppearの呼び出し vc.endAppearanceTransition() } } ProfileViewController.swift A89 viewWill/DidAppearを呼ぶには begin/endAppearanceTransitionをコールする

Slide 90

Slide 90 text

A90 本⽇のindex • @imaizumeとDiverseの紹介 • スナップショットテスト導⼊の背景 • 初期設定 • 撮影時の表⽰条件・状態整理 • 状態再現のための設計修正 • 撮影した情報の整理 • 様々なViewの撮影 • TIPS&ハマりポイント • まとめ

Slide 91

Slide 91 text

A91 スナップショットテスト導⼊の効果 ※残念ながら実際の⼯数計測はまだできていません エンジニア • 開発段階で表⽰バグに気づけるように • 撮影時の状態再現を意識して設計するように テスター • 表⽰確認の⼿間が減り全体の⼯数が半減 • 既存画⾯の継続的なデグレチェックが可能に • ⼿が回らなかった条件での表⽰バグを発⾒ 互いに表⽰確認作業の改善を実感

Slide 92

Slide 92 text

A92 想定と違ったところ (現時点では) 差分検知機能があまり使われない (導⼊から⽇数が経過していないため) 倜鋉歗꬗ 傀㶷歗꬗ 湡涸 歗꬗ؕةؚٗ⡲䧭 رؚٖ嗚濼 邌爙勴⟝ 㢌刿〳腉䚍֮׶ 然㹀幥׫ 何⥜؝أز ⡚ְ 넝ְֿהָ֮׷ 新規画⾯と既存画⾯での導⼊時の条件⽐較 ⻑期運⽤の中で効果が発揮されそう 運⽤段階で効果を発揮

Slide 93

Slide 93 text

A93 スナップショットテストすべき画⾯ =「導⼊効果が⾼そう」な画⾯ 考慮する状態が多い → とにかく画⾯カタログがほしい 実⾏時に再現が難しい・不安定 → 再現のための負荷が減る レイアウトが複雑 → 予期せぬ表⽰崩れが発⽣しやすい

Slide 94

Slide 94 text

A94 導⼊の是⾮を検討すべき画⾯ =「導⼊費⽤が⾼そう」な画⾯ 状態再現のための修正コストが⾼い → APIモックなどの戦略を検討する 仕様が未確定 or ⾼頻度で変わる → 仕様が変わるたび再撮影の必要

Slide 95

Slide 95 text

A95 今後の課題点 スクリーンカバレッジの向上 → 継続してテストを増やしていく必要あり  (Poiboyは約 10/100 ViewController) → 定期的に参考画像の履歴を削除するか git submodule化 / Dropbox等へ保存   (レポートをGitHubで⾒たいので悩む) リポジトリサイズ増加問題への対応

Slide 96

Slide 96 text

A96 まとめ ① スナップショットテストを使うと UIの撮影と崩れ検知を⾃動化できる ③ ⽬的の表⽰を再現するには 設計変更やAPIのモックが必要なことがある ② 撮影した画像をレポートにすると テスターとの情報共有がスムーズになる ④ ⻑期的に運⽤することで

Slide 97

Slide 97 text

A97 Special Thanks 株式会社Diverseのみなさん 株式会社UZUMAKIのみなさん 株式会社DeNA @kuniwak さん この発表を聴きに来てくれた皆さん スナップショットテスト実戦投⼊の 報告をお待ちしています!!

Slide 98

Slide 98 text

A98 最後に そういえば今週... ということは iOS 13 で表⽰確認しないと... (Dark Mode) なので 今すぐスナップショットテストを書こう❗ iOS 13 正式版が配信(されるはず) (iOSSnapshotTestCase / SnapshotTesting は XCode beta 6 & iOS 13.0 beta で動作を確認)

Slide 99

Slide 99 text

A99 参考リンク・書籍1 • 株式会社Diverse https://diverse-inc.co.jp/recruit/environment 働きやすい環境で真剣に出会いのプラットフォーム作ってます! • Poiboy https://poiboy.jp @imaizume も開発しているマッチングアプリ ぜひDLしてね!! • 株式会社Uzumaki https://uzumaki-inc.jp Webクリエイターのギルド集団 アプリやデザインのお仕事お待ちしています!! • iOSSnapshotTestCase https://github.com/uber/ios-snapshot-test-case/ Facebook製で現在はUberがメンテしているスナップショットテストフレームワーク • SnapshotTesting https://github.com/pointfreeco/swift-snapshot-testing/ Point-Freeの出しているSwift製スナップショットテストフレームワーク

Slide 100

Slide 100 text

A100 参考リンク・書籍2 • What is iOSSnapshotTestCase (@tamaki) https://speakerdeck.com/tamaki/what-is-iossnapshottestcase 基礎的な事項が分かりやすくまとめられているのでぜひ併せて読んでみてください! • Snapshot Testing in iOS (@suieyy) https://speakerdeck.com/susieyy/snapshot-testing-in-ios 概要から運⽤ノウハウについて分かりやすく書かれています、こちらも必読です! • iOS Snapshot Testing (Aaina Jain) https://www.linkedin.com/pulse/ios-snapshot-testing-aaina-jain- スナップショットでの基礎的な説明内容はこちらを参考にさせていただきました • Instantiate https://github.com/tarunon/Instantiate ViewControllerやViewに簡単にDIできるようにするためのライブラリ • iOSアプリのテストを書きたいのに書けないあなたへ (@imaizume) https://speakerdeck.com/imaizume/how-you-should-start-to-write-your-first-unit- test-for-ios テストのための設計変更やリポジトリパターンについて解説した過去の発表

Slide 101

Slide 101 text

A101 参考リンク・書籍3 • folio-sec/Fastfile https://github.com/folio-sec/Fastfile スナップショットに役⽴つ⼤変便利なlaneとスクリプトが揃ったライブラリです folioさんに感謝して使いましょう!! • @imaizume のMarkdownファイル⽣成スクリプト https://gist.github.com/imaizume/6aa2537c1c6778b50873c813eb7a15f1 folioさんのスクリプトを参考に⼀部を改変させていただきました • はじめてのfastlane Snapshot編 - Qiita (@tamaki) https://qiita.com/tamaki/items/f5e9f9985a91fb6a0f06 fastlane Snapshotについて詳しく知りたい⽅はこちらを参考に • CharlesでiOS Simulatorの通信をキャプチャする⽅法 - Qiita https://qiita.com/ruwatana/items/93cafe2369faec4b2598 今回解説できなかったChalesのインストールやキャプチャー⽅法はこちらをご参考に • Wiremockで⾏うUIテスト - Speaker Deck (@tamaki) https://speakerdeck.com/tamaki/wiremockdexing-uuitesuto Wiremockを使ったテストの仕⽅についてまとまっています

Slide 102

Slide 102 text

A102 参考リンク・書籍4 • [iOS、Swift] ユニットテストの時に、任意のタイミングでViewDidLoad()、 ViewWill(Did)Appear()、ViewWill(Did)Disappear()を呼び出す⽅法 - Qiita https://qiita.com/mii-chan/items/a9d8fd420d04b92a1c34 ライフサイクルメソッドを外部から操作する時に参考にさせていただきました • Failure image generated despite no change of code and their diff incorrectly rendered · Issue #99 - uber/ios-snapshot-test-case https://github.com/uber/ios-snapshot-test-case/issues/99 意図しないdiffが出てしまう問題を報告しています 返信がきたらどこかで共有します • Snapshot testing in XCTest - XCNotes - Medium https://medium.com/xcnotes/snapshot-testing-in-xcuitest-d18ca9bdeae iOSSnapshotTestCaseのpixel toleranceについて解説されています • switching to dark mode in UI test - Apple Developer Forums https://medium.com/xcnotes/snapshot-testing-in-xcuitest-d18ca9bdeae UIテストでDark Modeをエミュレーションすることはできるのか...

Slide 103

Slide 103 text

A103 補⾜ Snapshot Testing (swift-snapshot-testing)関連 発表時省略 ⏭

Slide 104

Slide 104 text

A104 iOSSnapshotTestCase VS SnapshotTesting // UIViewControllerの撮影時は画面サイズをassociated valueに渡せる let vc: UIViewController = .init() // iPhone SEで撮影 assertSnapshot(matching: vc, as: .image(on: .iPhoneSe)) // iPhone 8のランドスケープで撮影 assertSnapshot(matching: vc, as: .image(on: .iPhone8(.landscape))) // UIViewの撮影時は渡せない let view: UIView = .init(frame: size) // ERROR: Generic parameter 'Format' could not be inferred assertSnapshot(matching: view, as: .image(on: .iPhone8)) SnapshotTestingでのフォルダ・画像名の指定 端末サイズと回転情報をメソッド引数に渡せる (OSは実⾏時の端末に依存) 発表時省略 ⏭

Slide 105

Slide 105 text

ㄏせ鋉⵱ デフォルト (指定不可) {Testクラス} (例: MySnapshotTests) デフォルト {メソッド}.{通し番号}.png (例: testView.1.png) 指定時 (namedなし) {testName}.{通し番号}.png (例: プロフ基本表示.1.png) 指定時 (namedあり) {testName}.{named}.{通し番号}.png (例: プロフ基本表示.スクロール後.1.png) 갪湡 A105 フォルダ・ファイル名の命名規則 • フォルダ名指定が不可 • 画⾯サイズなどの環境情報は⾃前で付与する必要あり • メンバではなくテストメソッドの引数に指定する フォルダ 画像 発表時省略 ⏭

Slide 106

Slide 106 text

A106 フォルダ/画像名の指定 func testView() { // testView.1.png assertSnapshot(matching: vc, as: .image) // プロフィール.1.png assertSnapshot(matching: vc, as: .image, testName: "プロフィール") // プロフィール.スクロール後.1.png assertSnapshot(matching: vc, as: .image, named: "スクロール後", testName: "プロフィール") } SnapshotTestingでのフォルダ・画像名の指定 発表時省略 ⏭

Slide 107

Slide 107 text

A107 補⾜ うまくいかなかったこと 発表時省略 ⏭

Slide 108

Slide 108 text

A108 差がないのにDiffが出⼒されてしまうバグ❓ • 特定画⾯で、撮影直後に続けて差分⽐較しても なぜかテストが失敗し差分も出⼒されてしまう • diffではfail側が⼩さく表⽰されてしまう reference failure diff 発表時省略 ⏭

Slide 109

Slide 109 text

A109 作者に問い合わせてみたが... https://github.com/uber/ios-snapshot-test-case/issues/99 残念ながら返答なし しかし仲間はいた 発表時省略 ⏭

Slide 110

Slide 110 text

A110 モザイクをかけたViewがうまく撮影されない 本来の表⽰ スナップショットで撮影された表⽰ let frame: CGRect = .init(origin: .zero, size: .init(width: 150, height: 30)) let label: UILabel = .init(frame: frame) label.layer.shouldRasterize = true label.layer.magnificationFilter = .nearest label.layer.minificationFilter = .trilinear label.layer.rasterizationScale = 0.3 assertSnapshot(matching: label.layer, as: .image) ラベルにモザイクをかける iOSSnapshotTestCase / SnapshotTestingともに不可 XCUITestならうまく撮影できるかも (未検証) 発表時省略 ⏭

Slide 111

Slide 111 text

A111 補⾜ その他 発表時省略 ⏭

Slide 112

Slide 112 text

A112 CIへの組み込み (Bitrise) 発表時省略 ⏭ 単⼀環境で実⾏ 複数環境で実⾏ (Fastlane) 通常の単体テストと 同じフローでOK (未検証ですが) P74のスクリプトの実⾏だけでは Attachementにならない FastlaneからGitHub APIに markdownをPOSTすれば PRにレポートを出⼒できるかも 差分画像がzipでDL可

Slide 113

Slide 113 text

A113 Fastlane Snapshotじゃだめなの? iOSSnapshotTestCaseなどと同じ スクリーンショット撮影⽤ツール • ⽬的の画⾯での状態再現が難しい • E2Eテストに近いため壊れやすい • 差分⽐較の機能がない XCUITest上で動作する 発表時省略 ⏭

Slide 114

Slide 114 text

import WiremockClient class SnapshotTestWithWiremockClient: FBSnapshotTestCase { override func setUp() { WiremockClient.postMapping(stubMapping: StubMapping .stubFor(requestMethod: .GET, urlMatchCondition: .urlPathMatching, url: "/api/v3/fetch/some") .willReturn(ResponseDefinition() .withStatus(200) .withLocalJsonBodyFile(fileName: "Profile1", fileBundleId: bundleIdentifier, fileSubdirectory: nil)))} ... } WiremockClientの設定例 A114 Wiremockを使ったAPIのモック • WiremockはローカルでAPIをモックできるツール • クライアント側はWiremockClientをインストール • ローカルでの動作確認に有効 発表時省略 ⏭

Slide 115

Slide 115 text

A115 JSONファイルの区別がつかなくなる問題 • 各ファイルが表す表⽰条件を把握しづらい • しかしJSONにはコメントが書けない { "memo": { "目的": "長いプロフ検証", "職業": "2文字", "名前": "15文字", "プロフ": "長い", "共通点": 6 }, "body": {...} } ProfileResponseModel.1.json 適当なkeyを掘ってメモするとちょっと便利 発表時省略 ⏭

Slide 116

Slide 116 text

A116 ローカルのJSONファイルを読み込む { "result": { "member": { "age": 26, "name": 0, "work": "IT", "prefecture": "東京", "introduction": "プロフィールを見ていただきありがとう...", ... }, ... } Profile.json // Bundle.main.pathで取得 let json = Bundle.main.path(forResource: "Profile", ofType: "json")! // SwiftGenで取得 let json = JSONFiles.Profile.result Sample.swift 発表時省略 ⏭

Slide 117

Slide 117 text

A117 リソース管理ライブラリ(SwiftGen) • SwiftGenやR.swiftだとローカルのJSONファイルを Type Safeに取得できるので便利 • SwiftGenは定義した値をループで回せないので注意 (static定数に定義するので) { "body": { "name": "今泉", "age": 28 }, "extra": true } Profile.json internal enum JSONFiles { internal enum Profile { internal static let extra: Bool = true internal static let body: [String: Any] = ["age": 28, "name": "今泉"] } } JSON.swift SwiftGenを利⽤してJSONをType Safeに取得する例 github.com/SwiftGen/SwiftGen 発表時省略 ⏭

Slide 118

Slide 118 text

A118 identifierに⼀部の記号が使えない ⼀部記号が _ に変換されてしまう FBSnapshotVerifyView(vc.view, identifier: "!#$%&'()=-~¥|[]{}`@*+;:/?<>,.") identifierに記号を含めた場合 testXXX___$_____=_~¥|____`__+____<>[email protected] identifierに複雑な情報を含めて レポートスクリプトでparseするのはおすすめしない 区切り⽂字に記号を使おうとしたら (出⼒される画像名) (上記はiOSSnapshotTestCaseの場合) 発表時省略 ⏭

Slide 119

Slide 119 text

A119 pixel toleranceについて • 特定のOSや端末でわずかにフォントが変わる • 振動するようなアニメーションを撮影する 差分検知で許容される表⽰のずれ pixel toleranceを利⽤するシーン // 画像全体で5%のピクセルが合致しないことを許容する FBSnapshotVerifyView(vc.view, identifier: "view" , overallTolerance: 0.05) // 各ピクセル単位でRGBAによる色距離の差を5%まで許容する FBSnapshotVerifyView(vc.view, identifier: "view" , perPixelTolerance: 0.05) pixel toleranceを指定する例 https://medium.com/xcnotes/snapshot-testing-in-xcuitest-d18ca9bdeae https://github.com/facebookarchive/ios-snapshot-test-case/blob/ master/FBSnapshotTestCase/FBSnapshotTestCase.h 発表時省略 ⏭