Slide 1

Slide 1 text

Snapshot Testing ɹ ɹ ɹ ɹ iOS test Night #10 2019/04/16@גࣜձࣾσΟʔɾΤψɾΤʔ Yohei Suginami ( @susieyy )

Slide 2

Slide 2 text

Profile — Yohei Sugigami — @susieyy — Twitter / Github / Qiita — Freelance iOS App Developer — @ FOLIO Co., Ltd.

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

ɹ ɹ εφοϓγϣοτ ΠϝʔδUIςετ

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

ϦάϨογϣϯςετʢճؼςετʣ — ϓϩάϥϜͷมߋʹ൐͍ɺγεςϜʹ༧૝֎ͷӨڹ͕ݱΕͯ ͍ͳ͍͔Ͳ͏͔Λ֬ೝ͢Δςετ — Snapshot Image UI Test͸UI͕༧ظͤͣมߋ͞Ε͍ͯͳ͍ ͔Λ͔֬ΊΔͷʹඇৗʹ༗༻ͳπʔϧ

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

৽ن࡞੒ը໘ͷϨϏϡʔෛՙܰݮ εϞʔΫςετʢಈ࡞֬ೝʣͷҰ෦ΛࣗಈԽ — ҟͳΔσόΠεʢը໘αΠζʣɺOSຖͷಈ࡞֬ೝ — ڥք஋ɾ࠷େ஋ͷಈ࡞֬ೝ — ҟৗܥ΍ಈ࡞֬ೝʹ͢ΔͨΊʹঢ়ଶΛ࠶ݱ͢Δͷ͕೉͍͠ ঢ়ଶͷಈ࡞֬ೝ

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

ը໘Χλϩά — ঢ়ଶຖʹͲ͏Α͏ͳը໘ͷදࣔͱͳΔͷ͔֬ೝ͕༰қ — σβΠϯ࢓༷ͷը໘ҰཡʢSketchͳͲʣͰ͸͢΂ͯͷද ࣔύλʔϯΛ໢ཏͨ͠ը໘σβΠϯ͕ͳ͍৔߹΋͋Δ — ৽͍͠ࢀՃϝϯόʔͷΩϟοϓΞοϓͰ׆༻ — ো֐࣌ʹ͋Δঢ়ଶʹ͓͚Δදࣔ֬ೝɾ໰୊੾Γ෼͚Ͱ׆༻ — GihubϦϙδτϦʹMARKDOWNΛࣗಈੜ੒

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

ը໘Χλϩάͱͯ͠ͷ Fastlane snapshotͱͷҧ͍ Fastlane snapshot͸UITestingΛར༻͓ͯ͠ΓɺϢʔβͷૢ ࡞ΛਅࣅͨίʔυͰද͍ࣔͨ͠ը໘ʹભҠ͢Δ — ಛఆͷ৚݅Լʹ͓͚Δը໘දࣔΛ࠶ݱ͠ʹ͍͘ — ಛఆͷ৚݅Լʹ͓͚ΔAPI ResponseͷMockΛࠩ͠ࠐΈʹ ͍͘ — E2Eςετ૬౰ͷӡ༻ίετ͔͔ΔʢյΕ΍͍͢ʣ

Slide 19

Slide 19 text

ɹ ɹ iOS SnapshotTestCase

Slide 20

Slide 20 text

iOS SnapshotTestCase — ςετର৅ͷUIView·ͨ͸CALayerͷεφοϓγϣοτը ૾ΛࡱΓɺࣄલʹ४උͨ͠ϦϑΝϨϯεը૾ͱࠩ෼Λൺֱ͠ ͯҰக͢Δ͔Λ֬ೝ͢ΔςεςΟϯάϑϨʔϜϫʔΫ — XCTestCase಺Ͱར༻Ͱ͖Δ — Facebook͕࡞੒ͨOSS͕ͩͬͨUber͕Ҿ͖ܧ͗ — https://github.com/uber/ios-snapshot-test-case

Slide 21

Slide 21 text

ಋೖํ๏ɾར༻ํ๏ — What is iOSSnapshotTestCase — https://speakerdeck.com/tamaki/what-is- iossnapshottestcase?slide=35 — Shingo Tamaki — ಋೖํ๏͔Βجૅతͳ࢖͍ํ·Ͱஸೡʹղઆ͞Ε͓ͯΓΦ εεϝ

Slide 22

Slide 22 text

ςετίʔυྫ 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) } }

Slide 23

Slide 23 text

ςετίʔυྫ 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]) } }

Slide 24

Slide 24 text

ϦϑΝϨϯεը૾Λ࡞੒ — FastlaneͰεΫϦϓτԽͯ͠Ұൃ࣮ߦ — NSProcessInfoܦ༝ͰrecordModeΛCLI͔Β஫ೖ — ଟ༷ͳσόΠεͱOSόʔδϣϯͷ૊Έ߹ΘͤͰੜ੒ — iPhoneSE, iPhoneX, iPhoneX MAX — iOS10, iOS11, iOS12

Slide 25

Slide 25 text

௨৴ͷϨεϙϯεΛMockԽ(1/2) — URLSessionͷϨεϙϯεΛMockԽ — ϓϩμΫγϣϯίʔυ͸ඇഁյ — OHHTTPStubs΍Mockingjayʢϊʔϝϯςʣ

Slide 26

Slide 26 text

௨৴ͷϨεϙϯεΛMockԽ(2/2) — ࢦఆͨ͠ϦΫΤετʢύε΍ύϥϝʔλʣ͕Ϛονͨ͠৔ ߹ʹ೚ҙͷϨεϙϯεΛฦ͢Α͏ʹมߋͰ͖Δ — status 200 & JSON body or status 500 — Ϩεϙϯεͷdelay΋ઃఆՄೳ — ௨৴தͷঢ়ଶ΍ϦΫΤετλΠϜΞ΢τͷ֬ೝ — ը૾΋ઃఆՄೳ

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

ɹ ɹ ೰·͍͠໰୊

Slide 29

Slide 29 text

CI͚ͩTest͕Fail͢Δ໰୊͕Ұ࣌ظൃੜ (1/2) — BitriseͷϚγϯϦιʔεෆ଍͕ݪҼͩͬͨ — ૝ఆ͍ͯ͠Δ΢ΣΠτ࣌ؒ಺ʹॲཧʢΞχϝʔγϣϯʣ͕ ׬ྃͤͣϦϑΝϨϯεը૾ͱࠩ෼͕ग़ͯ͠·͏ — खݩͷϚγϯͰ͸ৗʹύε͢Δ — Bitrise͕ϚγϯϦιʔεΛΞοϓάϨʔυͨ͠ͷͰվળ Update on Mac infrastructure upgrades and queues @ March 14, 2019 - https://blog.bitrise.io/update-mac-infrastructure-

Slide 30

Slide 30 text

CI͚ͩTest͕Fail͢Δ໰୊͕Ұ࣌ظൃੜ (2/2) — WaitΛνϡʔχϯάͯ͠ෛՙ͕ى͖ͯ΋յΕʹ͘͘͢Δ — ঢ়ଶมԽޙͷඳը΍Ξχϝʔγϣϯॲཧ෦෼ͷΈదٓ wait(etc 0.5sec)ΛೖΕͯௐ੔ — ϕʔεͷwait࣌ؒΛڞ௨Խ — RxBlockingΛ׆༻͠ඇಉظͰঢ়ଶ͕มԽ͠ऴΘΔͱ͜Ζ· Ͱ͸BlockingͰWait

Slide 31

Slide 31 text

ΞχϝʔγϣϯதʹεΫϦʔϯγϣοτΛऔಘͯ͠͠·͏ͱϦ ϑΝϨϯεը૾ͱࠩ෼͕ग़ͯ͠·͏ e.g, Kingfisherͷը૾දࣔ࣌ͷϑΣʔυΞχϝʔγϣϯΛTest ࣌ͷΈOFF KingfisherManager.shared.defaultOptions = [.transition(.none)] e.g, NotificationBannerΛΞχϝʔγϣϯΛTest࣌ͷΈOFF

Slide 32

Slide 32 text

UI͸࢓༷ɾཁ͕݅มΘΓ΍͍͢ͷͰςετ͕յΕ΍͍͢ ϝϦοτ — յΕΔ͜ͱͰҙਤ͠ͳ͍ӨڹൣғͰσάϨʔγϣϯ͍ͯ͠ Δ͜ͱʹؾ͚ͮΔ σϝϦοτ — ը໘ͷද͕ࣔมΘΔͱεΫϦʔϯγϣοτΛऔΓ௚ͨ͠ Γɺςετέʔε΍MockσʔλΛमਖ਼ͨ͠Γͱϝϯςφϯ είετ͕͔͔Δ

Slide 33

Slide 33 text

UI͸࢓༷ɾཁ͕݅มΘΓ΍͍͢ͷͰςετ͕յΕ΍͍͢ τϨʔυΦϑ — αʔϏεʹ͓͚ΔROIʢඅ༻ରޮՌʣΛؑΈͯಋೖՄ൱Λݕ ౼ͨ͠Γɺ༻్Λݶఆ͢ΔʢҟৗܥͷΈεΫϦʔϯγϣο τςετ͢ΔʣͳͲͯ͠ɺ͏·͘׆༻͢Δඞཁ͕͋Δ

Slide 34

Slide 34 text

ɹ ɹ Snapshot Testing ɹ ɹ ɹ Stephen Celis

Slide 35

Slide 35 text

ɹ ɹ Snapshot Anything

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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) // , state: appeared, view: // | , state: appeared, view: // | | , state: appeared, view: <_UIPageViewControllerContentView> // | | | , state: appeared, view: // | , state: disappeared, view: not in the window // | | , state: disappeared, view: (view not loaded)

Slide 39

Slide 39 text

Referenceͷॻ͖ग़͠ import SnapshotTesting import XCTest class HogeeTests: XCTestCase { func testView() { record = true let vc = MyViewController() assertSnapshot(matching: vc, as: .image) } }

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

URLRequest assertSnapshot(matching: urlRequest, as: .raw) // POST http://localhost:8080/account // Cookie: pf_session={"userId":"1"} // // email=blob%40pointfree.co&name=Blob

Slide 42

Slide 42 text

JSON assertSnapshot(matching: user, as: .json) // { // "bio" : "Blobbed around the world.", // "id" : 1, // "name" : "Blobby" // }

Slide 43

Slide 43 text

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.func(into: .description) ) // "up","left" // "down","right" // "left","down" // "right","up"

Slide 44

Slide 44 text

Any assertSnapshot(matching: user, as: .dump) // ▿ User // - bio: "Blobbed around the world." // - id: 1 // - name: "Blobby"

Slide 45

Slide 45 text

Failed Diff σʔλͷࠩ෼͕දࣔ͞ΕΔͷͰΘ͔Γ΍͍͢ footer: https://www.stephencelis.com/2017/09/snapshot-testing-in-swift

Slide 46

Slide 46 text

Defining Custom Snapshot Strategies extension Snapshotting where Value == WKWebView, Format == UIImage { public static let image: Snapshotting = Snapshotting.image .asyncPullback { webView in Async { callback in webView.takeSnapshot(with: nil) { image, error in callback(image!) } } } }

Slide 47

Slide 47 text

ࣄྫ঺հ1 ImagePipeline by Katsumi Kishikawa — Image Pipeline is an image loading and caching framework — σίʔυͨ͠ը૾΍Ճ޻ॲཧΛͨ͠ը૾ͷൺֱςετͰར ༻ — https://github.com/folio-sec/ImagePipeline

Slide 48

Slide 48 text

ࣄྫ঺հ2 SwiftRewriter by Yasuhiro Inami — Swift code formatter using SwiftSyntax. — ΧελϜετϥςδʔΛ࡞੒͠ϑΥʔϚοτޙͷSwingίʔ υͱϦϑΝϨϯεSwingίʔυΛൺֱ — https://github.com/inamiy/SwiftRewriter

Slide 49

Slide 49 text

No content

Slide 50

Slide 50 text

·ͱΊ εφοϓγϣοτΠϝʔδUIςετ͸ඞཁੑɺඅ༻ର߅Λݕ౼ ͯ͠ಋೖ - ಋೖͱςετΛॻ͖ग़͢͜ͱ͸༰қͳͷͰখ࢝͘͞ΊΒΕΔ - E2EςετΑΓ͸ಋೖɾӡ༻ίετ͸௿͍ - ·ͣ͸DomainςετΛ͔ͬ͠Γॻ͘͜ͱ͕༏ઌ Snapshot Testing͸Ϣχοτςετͷ෯Λ޿͛ΒΕΔͷͰΦ εεϝ