$30 off During Our Annual Pro Sale. View Details »

Snapshot Testing in iOS

Snapshot Testing in iOS

yohei sugigami

April 16, 2019
Tweet

More Decks by yohei sugigami

Other Decks in Technology

Transcript

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

    View Slide

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

    View Slide

  3. View Slide

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

    View Slide

  5. View Slide

  6. View Slide

  7. View Slide

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

    View Slide

  9. View Slide

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

    View Slide

  11. View Slide

  12. View Slide

  13. View Slide

  14. View Slide

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

    View Slide

  16. View Slide

  17. View Slide

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

    View Slide

  19. ɹ
    ɹ
    iOS SnapshotTestCase

    View Slide

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

    View Slide

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

    View Slide

  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)
    }
    }

    View Slide

  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])
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  27. View Slide

  28. ɹ
    ɹ
    ೰·͍͠໰୊

    View Slide

  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-

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  34. ɹ
    ɹ
    Snapshot Testing
    ɹ
    ɹ
    ɹ
    Stephen Celis

    View Slide

  35. ɹ
    ɹ
    Snapshot Anything

    View Slide

  36. View Slide

  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

    View Slide

  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)
    // , 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)

    View Slide

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

    View Slide

  40. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  46. 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!)
    }
    }
    }
    }

    View Slide

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

    — https://github.com/folio-sec/ImagePipeline

    View Slide

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

    View Slide

  49. View Slide

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

    View Slide