Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Speaker Deck
PRO
Sign in
Sign up
for free
Snapshot Testing in iOS
yohei sugigami
April 16, 2019
Technology
6
1.3k
Snapshot Testing in iOS
yohei sugigami
April 16, 2019
Tweet
Share
More Decks by yohei sugigami
See All by yohei sugigami
susieyy
0
650
susieyy
5
1.2k
susieyy
8
980
susieyy
10
1.3k
susieyy
14
5.3k
susieyy
0
3k
susieyy
7
5.8k
susieyy
0
440
susieyy
5
370
Other Decks in Technology
See All in Technology
kkosukeee
0
160
shirayanagiryuji
1
280
kaniza
0
120
shoichiron
1
150
gamella
3
1.5k
ryomasumura
0
120
chaspy
6
1.3k
ch1aki
2
240
robcrowley
0
160
udzura
2
260
900groove
2
520
sansandsoc
0
340
Featured
See All Featured
zenorocha
296
40k
reverentgeek
27
2k
keathley
20
710
scottboms
251
11k
zakiwarfel
88
3.4k
bermonpainter
342
26k
chriscoyier
145
20k
yeseniaperezcruz
302
31k
3n
163
22k
skipperchong
8
720
colly
188
14k
robhawkes
52
2.8k
Transcript
Snapshot Testing ɹ ɹ ɹ ɹ iOS test Night #10
2019/04/16@גࣜձࣾσΟʔɾΤψɾΤʔ Yohei Suginami ( @susieyy )
Profile — Yohei Sugigami — @susieyy — Twitter / Github
/ Qiita — Freelance iOS App Developer — @ FOLIO Co., Ltd.
None
ɹ ɹ εφοϓγϣοτ ΠϝʔδUIςετ
None
None
None
ϦάϨογϣϯςετʢճؼςετʣ — ϓϩάϥϜͷมߋʹ͍ɺγεςϜʹ༧֎ͷӨڹ͕ݱΕͯ ͍ͳ͍͔Ͳ͏͔Λ֬ೝ͢Δςετ — Snapshot Image UI TestUI͕༧ظͤͣมߋ͞Ε͍ͯͳ͍ ͔Λ͔֬ΊΔͷʹඇৗʹ༗༻ͳπʔϧ
None
৽ن࡞ը໘ͷϨϏϡʔෛՙܰݮ εϞʔΫςετʢಈ࡞֬ೝʣͷҰ෦ΛࣗಈԽ — ҟͳΔσόΠεʢը໘αΠζʣɺOSຖͷಈ࡞֬ೝ — ڥքɾ࠷େͷಈ࡞֬ೝ — ҟৗܥಈ࡞֬ೝʹ͢ΔͨΊʹঢ়ଶΛ࠶ݱ͢Δͷ͕͍͠ ঢ়ଶͷಈ࡞֬ೝ
None
None
None
None
ը໘Χλϩά — ঢ়ଶຖʹͲ͏Α͏ͳը໘ͷදࣔͱͳΔͷ͔֬ೝ͕༰қ — σβΠϯ༷ͷը໘ҰཡʢSketchͳͲʣͰͯ͢ͷද ࣔύλʔϯΛཏͨ͠ը໘σβΠϯ͕ͳ͍߹͋Δ — ৽͍͠ࢀՃϝϯόʔͷΩϟοϓΞοϓͰ׆༻ — ো࣌ʹ͋Δঢ়ଶʹ͓͚Δදࣔ֬ೝɾΓ͚Ͱ׆༻
— GihubϦϙδτϦʹMARKDOWNΛࣗಈੜ
None
None
ը໘Χλϩάͱͯ͠ͷ Fastlane snapshotͱͷҧ͍ Fastlane snapshotUITestingΛར༻͓ͯ͠ΓɺϢʔβͷૢ ࡞ΛਅࣅͨίʔυͰද͍ࣔͨ͠ը໘ʹભҠ͢Δ — ಛఆͷ݅Լʹ͓͚Δը໘දࣔΛ࠶ݱ͠ʹ͍͘ — ಛఆͷ݅Լʹ͓͚ΔAPI
ResponseͷMockΛࠩ͠ࠐΈʹ ͍͘ — E2Eςετ૬ͷӡ༻ίετ͔͔ΔʢյΕ͍͢ʣ
ɹ ɹ iOS SnapshotTestCase
iOS SnapshotTestCase — ςετରͷUIView·ͨCALayerͷεφοϓγϣοτը ૾ΛࡱΓɺࣄલʹ४උͨ͠ϦϑΝϨϯεը૾ͱࠩΛൺֱ͠ ͯҰக͢Δ͔Λ֬ೝ͢ΔςεςΟϯάϑϨʔϜϫʔΫ — XCTestCaseͰར༻Ͱ͖Δ — Facebook͕࡞ͨOSS͕ͩͬͨUber͕Ҿ͖ܧ͗
— https://github.com/uber/ios-snapshot-test-case
ಋೖํ๏ɾར༻ํ๏ — What is iOSSnapshotTestCase — https://speakerdeck.com/tamaki/what-is- iossnapshottestcase?slide=35 — Shingo
Tamaki — ಋೖํ๏͔Βجૅతͳ͍ํ·Ͱஸೡʹղઆ͞Ε͓ͯΓΦ εεϝ
ςετίʔυྫ import FBSnapshotTestCase class FBSnapshotTestCaseSwiftTest: FBSnapshotTestCase { override func setUp()
{ super.setUp() // ↓ true ʹ͢ΔͱςετͰͳ͘ϦϑΝϨϯεը૾Λग़ྗ recordMode = false } func testExample() { let vc = UIViewController() vc.view.size = CGSize(width: 370, height: 675) FBSnapshotVerifyView(vc.view) } }
ςετίʔυྫ class HogeViewControllerTests: SnapshotTestCase { func testAuthenticated() { stub(uri("/api/endpoins1"), jsonData(fixtureData("test_data1.json")))
stub(uri("/api/endpoins2"), jsonData(fixtureData("test_data2.json"))) let store = environment.createRedux() login(store) let viewController = HogeViewController(reduxStore: store) let navigationController = UINavigationController(rootViewController: viewController) viewController.request() verifyViewController(navigationController) verifyViewController(viewController, identifier: "fullscreen", options: [.fullscreen]) } }
ϦϑΝϨϯεը૾Λ࡞ — FastlaneͰεΫϦϓτԽͯ͠Ұൃ࣮ߦ — NSProcessInfoܦ༝ͰrecordModeΛCLI͔Βೖ — ଟ༷ͳσόΠεͱOSόʔδϣϯͷΈ߹ΘͤͰੜ — iPhoneSE, iPhoneX,
iPhoneX MAX — iOS10, iOS11, iOS12
௨৴ͷϨεϙϯεΛMockԽ(1/2) — URLSessionͷϨεϙϯεΛMockԽ — ϓϩμΫγϣϯίʔυඇഁյ — OHHTTPStubsMockingjayʢϊʔϝϯςʣ
௨৴ͷϨεϙϯεΛMockԽ(2/2) — ࢦఆͨ͠ϦΫΤετʢύεύϥϝʔλʣ͕Ϛονͨ͠ ߹ʹҙͷϨεϙϯεΛฦ͢Α͏ʹมߋͰ͖Δ — status 200 & JSON body
or status 500 — ϨεϙϯεͷdelayઃఆՄೳ — ௨৴தͷঢ়ଶϦΫΤετλΠϜΞτͷ֬ೝ — ը૾ઃఆՄೳ
None
ɹ ɹ ·͍͠
CI͚ͩTest͕Fail͢Δ͕Ұ࣌ظൃੜ (1/2) — BitriseͷϚγϯϦιʔεෆ͕ݪҼͩͬͨ — ఆ͍ͯ͠ΔΣΠτ࣌ؒʹॲཧʢΞχϝʔγϣϯʣ͕ ྃͤͣϦϑΝϨϯεը૾ͱ͕ࠩग़ͯ͠·͏ — खݩͷϚγϯͰৗʹύε͢Δ —
Bitrise͕ϚγϯϦιʔεΛΞοϓάϨʔυͨ͠ͷͰվળ Update on Mac infrastructure upgrades and queues @ March 14, 2019 - https://blog.bitrise.io/update-mac-infrastructure-
CI͚ͩTest͕Fail͢Δ͕Ұ࣌ظൃੜ (2/2) — WaitΛνϡʔχϯάͯ͠ෛՙ͕ى͖ͯյΕʹ͘͘͢Δ — ঢ়ଶมԽޙͷඳըΞχϝʔγϣϯॲཧ෦ͷΈదٓ wait(etc 0.5sec)ΛೖΕͯௐ — ϕʔεͷwait࣌ؒΛڞ௨Խ
— RxBlockingΛ׆༻͠ඇಉظͰঢ়ଶ͕มԽ͠ऴΘΔͱ͜Ζ· ͰBlockingͰWait
ΞχϝʔγϣϯதʹεΫϦʔϯγϣοτΛऔಘͯ͠͠·͏ͱϦ ϑΝϨϯεը૾ͱ͕ࠩग़ͯ͠·͏ e.g, Kingfisherͷը૾දࣔ࣌ͷϑΣʔυΞχϝʔγϣϯΛTest ࣌ͷΈOFF KingfisherManager.shared.defaultOptions = [.transition(.none)] e.g, NotificationBannerΛΞχϝʔγϣϯΛTest࣌ͷΈOFF
UI༷ɾཁ͕݅มΘΓ͍͢ͷͰςετ͕յΕ͍͢ ϝϦοτ — յΕΔ͜ͱͰҙਤ͠ͳ͍ӨڹൣғͰσάϨʔγϣϯ͍ͯ͠ Δ͜ͱʹؾ͚ͮΔ σϝϦοτ — ը໘ͷද͕ࣔมΘΔͱεΫϦʔϯγϣοτΛऔΓͨ͠ ΓɺςετέʔεMockσʔλΛमਖ਼ͨ͠Γͱϝϯςφϯ είετ͕͔͔Δ
UI༷ɾཁ͕݅มΘΓ͍͢ͷͰςετ͕յΕ͍͢ τϨʔυΦϑ — αʔϏεʹ͓͚ΔROIʢඅ༻ରޮՌʣΛؑΈͯಋೖՄ൱Λݕ ౼ͨ͠Γɺ༻్Λݶఆ͢ΔʢҟৗܥͷΈεΫϦʔϯγϣο τςετ͢ΔʣͳͲͯ͠ɺ͏·͘׆༻͢Δඞཁ͕͋Δ
ɹ ɹ Snapshot Testing ɹ ɹ ɹ Stephen Celis
ɹ ɹ Snapshot Anything
None
UIViewController ը໘Πϝʔδൺֱ assertSnapshot(matching: vc, as: .image) assertSnapshot(matching: vc, as: .image(on:
.iPhoneSe)) assertSnapshot(matching: vc, as: .image(on: .iPhoneSe(.landscape))) assertSnapshot(matching: vc, as: .image(on: .iPhoneX)) assertSnapshot(matching: vc, as: .image(on: .iPadMini(.portrait))) — ෦ͰWindowͱRootViewController͕࡞͘ΒΕaddChild — VCͷϥΠϑαΠΫϧίʔϧόοΫ͞ΕΔ — viewWillAppear, vieDidAppear
UIViewController ViewͷϑϨʔϜͱώΤϥϧΩʔൺֱ assertSnapshot(matching: vc, as: .recursiveDescription) assertSnapshot(matching: vc, as: .recursiveDescription(on:
.iPhoneSe)) assertSnapshot(matching: vc, as: .recursiveDescription(on: .iPhoneSe(.landscape))) assertSnapshot(matching: vc, as: .recursiveDescription(on: .iPhoneX)) assertSnapshot(matching: vc, as: .recursiveDescription(on: .iPadMini(.portrait))) // [ AF LU ] h=--- v=--- NSButton "Push Me" f=(0,0,77,32) b=(-) // [ A LU ] h=--- v=--- NSButtonBezelView f=(0,0,77,32) b=(-) // [ AF LU ] h=--- v=--- NSButtonTextField "Push Me" f=(10,6,57,16) b=(-) // A=autoresizesSubviews, C=canDrawConcurrently, D=needsDisplay, F=flipped, G=gstate,... assertSnapshot(matching: vc, as: .hierarchy) // <UITabBarController>, state: appeared, view: <UILayoutContainerView> // | <UINavigationController>, state: appeared, view: <UILayoutContainerView> // | | <UIPageViewController>, state: appeared, view: <_UIPageViewControllerContentView> // | | | <UIViewController>, state: appeared, view: <UIView> // | <UINavigationController>, state: disappeared, view: <UILayoutContainerView> not in the window // | | <UIViewController>, state: disappeared, view: (view not loaded)
Referenceͷॻ͖ग़͠ import SnapshotTesting import XCTest class HogeeTests: XCTestCase { func
testView() { record = true let vc = MyViewController() assertSnapshot(matching: vc, as: .image) } }
None
URLRequest assertSnapshot(matching: urlRequest, as: .raw) // POST http://localhost:8080/account // Cookie:
pf_session={"userId":"1"} // // email=blob%40pointfree.co&name=Blob
JSON assertSnapshot(matching: user, as: .json) // { // "bio" :
"Blobbed around the world.", // "id" : 1, // "name" : "Blobby" // }
CaseIterable enum Direction: String, CaseIterable { case up, down, left,
right var rotatedLeft: Direction { switch self { case .up: return .left case .down: return .right case .left: return .down case .right: return .up } } } assertSnapshot( matching: { $0.rotatedLeft }, as: Snapshotting<Direction, String>.func(into: .description) ) // "up","left" // "down","right" // "left","down" // "right","up"
Any assertSnapshot(matching: user, as: .dump) // ▿ User // -
bio: "Blobbed around the world." // - id: 1 // - name: "Blobby"
Failed Diff σʔλͷ͕ࠩදࣔ͞ΕΔͷͰΘ͔Γ͍͢ footer: https://www.stephencelis.com/2017/09/snapshot-testing-in-swift
Defining Custom Snapshot Strategies extension Snapshotting where Value == WKWebView,
Format == UIImage { public static let image: Snapshotting = Snapshotting<UIImage, UIImage>.image .asyncPullback { webView in Async { callback in webView.takeSnapshot(with: nil) { image, error in callback(image!) } } } }
ࣄྫհ1 ImagePipeline by Katsumi Kishikawa — Image Pipeline is an
image loading and caching framework — σίʔυͨ͠ը૾ՃॲཧΛͨ͠ը૾ͷൺֱςετͰར ༻ — https://github.com/folio-sec/ImagePipeline
ࣄྫհ2 SwiftRewriter by Yasuhiro Inami — Swift code formatter using
SwiftSyntax. — ΧελϜετϥςδʔΛ࡞͠ϑΥʔϚοτޙͷSwingίʔ υͱϦϑΝϨϯεSwingίʔυΛൺֱ — https://github.com/inamiy/SwiftRewriter
None
·ͱΊ εφοϓγϣοτΠϝʔδUIςετඞཁੑɺඅ༻ର߅Λݕ౼ ͯ͠ಋೖ - ಋೖͱςετΛॻ͖ग़͢͜ͱ༰қͳͷͰখ࢝͘͞ΊΒΕΔ - E2EςετΑΓಋೖɾӡ༻ίετ͍ - ·ͣDomainςετΛ͔ͬ͠Γॻ͘͜ͱ͕༏ઌ Snapshot
TestingϢχοτςετͷ෯Λ͛ΒΕΔͷͰΦ εεϝ