Snapshot Testing in iOS

Snapshot Testing in iOS

Acbf3391de0494432a92221ffe89f34e?s=128

yohei sugigami

April 16, 2019
Tweet

Transcript

  1. Snapshot Testing ɹ ɹ ɹ ɹ iOS test Night #10

    2019/04/16@גࣜձࣾσΟʔɾΤψɾΤʔ Yohei Suginami ( @susieyy )
  2. Profile — Yohei Sugigami — @susieyy — Twitter / Github

    / Qiita — Freelance iOS App Developer — @ FOLIO Co., Ltd.
  3. None
  4. ɹ ɹ εφοϓγϣοτ ΠϝʔδUIςετ

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

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

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

    — GihubϦϙδτϦʹMARKDOWNΛࣗಈੜ੒
  16. None
  17. None
  18. ը໘Χλϩάͱͯ͠ͷ Fastlane snapshotͱͷҧ͍ Fastlane snapshot͸UITestingΛར༻͓ͯ͠ΓɺϢʔβͷૢ ࡞ΛਅࣅͨίʔυͰද͍ࣔͨ͠ը໘ʹભҠ͢Δ — ಛఆͷ৚݅Լʹ͓͚Δը໘දࣔΛ࠶ݱ͠ʹ͍͘ — ಛఆͷ৚݅Լʹ͓͚ΔAPI

    ResponseͷMockΛࠩ͠ࠐΈʹ ͍͘ — E2Eςετ૬౰ͷӡ༻ίετ͔͔ΔʢյΕ΍͍͢ʣ
  19. ɹ ɹ iOS SnapshotTestCase

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

    — https://github.com/uber/ios-snapshot-test-case
  21. ಋೖํ๏ɾར༻ํ๏ — What is iOSSnapshotTestCase — https://speakerdeck.com/tamaki/what-is- iossnapshottestcase?slide=35 — Shingo

    Tamaki — ಋೖํ๏͔Βجૅతͳ࢖͍ํ·Ͱஸೡʹղઆ͞Ε͓ͯΓΦ εεϝ
  22. ςετίʔυྫ 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) } }
  23. ςετίʔυྫ 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]) } }
  24. ϦϑΝϨϯεը૾Λ࡞੒ — FastlaneͰεΫϦϓτԽͯ͠Ұൃ࣮ߦ — NSProcessInfoܦ༝ͰrecordModeΛCLI͔Β஫ೖ — ଟ༷ͳσόΠεͱOSόʔδϣϯͷ૊Έ߹ΘͤͰੜ੒ — iPhoneSE, iPhoneX,

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

  26. ௨৴ͷϨεϙϯεΛMockԽ(2/2) — ࢦఆͨ͠ϦΫΤετʢύε΍ύϥϝʔλʣ͕Ϛονͨ͠৔ ߹ʹ೚ҙͷϨεϙϯεΛฦ͢Α͏ʹมߋͰ͖Δ — status 200 & JSON body

    or status 500 — Ϩεϙϯεͷdelay΋ઃఆՄೳ — ௨৴தͷঢ়ଶ΍ϦΫΤετλΠϜΞ΢τͷ֬ೝ — ը૾΋ઃఆՄೳ
  27. None
  28. ɹ ɹ ೰·͍͠໰୊

  29. CI͚ͩTest͕Fail͢Δ໰୊͕Ұ࣌ظൃੜ (1/2) — BitriseͷϚγϯϦιʔεෆ଍͕ݪҼͩͬͨ — ૝ఆ͍ͯ͠Δ΢ΣΠτ࣌ؒ಺ʹॲཧʢΞχϝʔγϣϯʣ͕ ׬ྃͤͣϦϑΝϨϯεը૾ͱࠩ෼͕ग़ͯ͠·͏ — खݩͷϚγϯͰ͸ৗʹύε͢Δ —

    Bitrise͕ϚγϯϦιʔεΛΞοϓάϨʔυͨ͠ͷͰվળ Update on Mac infrastructure upgrades and queues @ March 14, 2019 - https://blog.bitrise.io/update-mac-infrastructure-
  30. CI͚ͩTest͕Fail͢Δ໰୊͕Ұ࣌ظൃੜ (2/2) — WaitΛνϡʔχϯάͯ͠ෛՙ͕ى͖ͯ΋յΕʹ͘͘͢Δ — ঢ়ଶมԽޙͷඳը΍Ξχϝʔγϣϯॲཧ෦෼ͷΈదٓ wait(etc 0.5sec)ΛೖΕͯௐ੔ — ϕʔεͷwait࣌ؒΛڞ௨Խ

    — RxBlockingΛ׆༻͠ඇಉظͰঢ়ଶ͕มԽ͠ऴΘΔͱ͜Ζ· Ͱ͸BlockingͰWait
  31. ΞχϝʔγϣϯதʹεΫϦʔϯγϣοτΛऔಘͯ͠͠·͏ͱϦ ϑΝϨϯεը૾ͱࠩ෼͕ग़ͯ͠·͏ e.g, Kingfisherͷը૾දࣔ࣌ͷϑΣʔυΞχϝʔγϣϯΛTest ࣌ͷΈOFF KingfisherManager.shared.defaultOptions = [.transition(.none)] e.g, NotificationBannerΛΞχϝʔγϣϯΛTest࣌ͷΈOFF

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

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

  34. ɹ ɹ Snapshot Testing ɹ ɹ ɹ Stephen Celis

  35. ɹ ɹ Snapshot Anything

  36. None
  37. 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
  38. 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)
  39. Referenceͷॻ͖ग़͠ import SnapshotTesting import XCTest class HogeeTests: XCTestCase { func

    testView() { record = true let vc = MyViewController() assertSnapshot(matching: vc, as: .image) } }
  40. None
  41. URLRequest assertSnapshot(matching: urlRequest, as: .raw) // POST http://localhost:8080/account // Cookie:

    pf_session={"userId":"1"} // // email=blob%40pointfree.co&name=Blob
  42. JSON assertSnapshot(matching: user, as: .json) // { // "bio" :

    "Blobbed around the world.", // "id" : 1, // "name" : "Blobby" // }
  43. 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"
  44. Any assertSnapshot(matching: user, as: .dump) // ▿ User // -

    bio: "Blobbed around the world." // - id: 1 // - name: "Blobby"
  45. Failed Diff σʔλͷࠩ෼͕දࣔ͞ΕΔͷͰΘ͔Γ΍͍͢ footer: https://www.stephencelis.com/2017/09/snapshot-testing-in-swift

  46. 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!) } } } }
  47. ࣄྫ঺հ1 ImagePipeline by Katsumi Kishikawa — Image Pipeline is an

    image loading and caching framework — σίʔυͨ͠ը૾΍Ճ޻ॲཧΛͨ͠ը૾ͷൺֱςετͰར ༻ — https://github.com/folio-sec/ImagePipeline
  48. ࣄྫ঺հ2 SwiftRewriter by Yasuhiro Inami — Swift code formatter using

    SwiftSyntax. — ΧελϜετϥςδʔΛ࡞੒͠ϑΥʔϚοτޙͷSwingίʔ υͱϦϑΝϨϯεSwingίʔυΛൺֱ — https://github.com/inamiy/SwiftRewriter
  49. None
  50. ·ͱΊ εφοϓγϣοτΠϝʔδUIςετ͸ඞཁੑɺඅ༻ର߅Λݕ౼ ͯ͠ಋೖ - ಋೖͱςετΛॻ͖ग़͢͜ͱ͸༰қͳͷͰখ࢝͘͞ΊΒΕΔ - E2EςετΑΓ͸ಋೖɾӡ༻ίετ͸௿͍ - ·ͣ͸DomainςετΛ͔ͬ͠Γॻ͘͜ͱ͕༏ઌ Snapshot

    Testing͸Ϣχοτςετͷ෯Λ޿͛ΒΕΔͷͰΦ εεϝ