Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ツールとして利用するUIテスト #iosdc #a #前夜祭 /use UI Testing as a tool in iOSDC 2018

fromkk
August 30, 2018

ツールとして利用するUIテスト #iosdc #a #前夜祭 /use UI Testing as a tool in iOSDC 2018

iOSDC 2018 前夜祭で発表した内容です。

https://fortee.jp/iosdc-japan-2018/speaker/proposal/view/559ef049-ecdf-43bf-96db-6ce343a100e5 より

> Xcode 9よりXCTestにスクリーンショット撮影機能が実装されました。XCTestはテストコードから地域や言語を指定する事が可能で、xcodebuildで複数のシミュレーターでテストが実行可能なので、様々な画面サイズと条件のスクリーンを集める事が出来るのです。デザイン崩れ等はUIテストでも判別が難しい物の一つですが、半自動的にデザイン崩れを解消する事が可能になったので発表したいと思います。

参考リンク情報 1: WWDC 2015 UI Testing in Xcode https://developer.apple.com/videos/play/wwdc2015/406/
参考リンク情報 2: Lunch https://github.com/fromkk/Lunch とその発表 https://speakerdeck.com/fromkk/orecon-ios-ui-test-20171003
参考リンク情報 3: WWDC 2017 What's New in Testing https://developer.apple.com/videos/play/wwdc2017/409/
参考リンク情報 4: UI Testのシェルスクリプト https://gist.github.com/fromkk/95b7fd046a081da5bfac913e1c7b31a8
参考リンク情報 5: WWDC 2018 What's New in Testing https://developer.apple.com/videos/play/wwdc2018/403/
参考リンク情報 6: TestSummaries https://github.com/fromkk/TestSummaries とその解説(HTML ver) https://qiita.com/fromkk/items/38d8d4139053ae33773f
[PR] Timers Inc. では絶賛エンジニア募集しています!家族向けアプリ、カップル向けアプリや印刷・月額課金系のシステに興味の方いましたら是非お声がけください! https://timers-inc.com
[PR] 今朝ツイートしましたがリジェクトコンで様々な募集が始まりました! https://iosdcrc.firebaseapp.com/ どしどしご応募ください!

fromkk

August 30, 2018
Tweet

More Decks by fromkk

Other Decks in Programming

Transcript

  1. πʔϧͱͯ͠ར༻͢ΔUIςετ
    iOSDC 2018 લ໷ࡇ Track A
    !1

    View Slide

  2. Profile
    struct Profile {
    let name = "Kazuya Ueoka"
    let twitter = "@fromkk"
    let github = "fromkk"
    let qiita = "fromkk"
    let company = "Timers Inc."
    }

    !2

    View Slide

  3. ͜Μͳࣄແ͍Ͱ͔͢ʁ
    !3

    View Slide

  4. UIͷ࣮૷͕׬ྃ
    ͱࢥ͍͖΍
    !4

    View Slide

  5. ༷ʑͳ୺຤ͰͷσβΠϯ่Ε
    •ҟͳΔը໘αΠζ
    •iPhone͚ͩ͡Όͳ͘iPadͷରԠ
    ଞʹ΋
    •ԣɾॎͷ੾Γସ͑
    •ҟͳΔOSόʔδϣϯ
    •ӳޠ͸จষ͕௕͘ͳΓ͕ͪ໰୊
    !5

    View Slide

  6. ϨΠΞ΢τ่ΕΛͳΔ΂͘ૣ͍ஈ֊Ͱݕ஌͍ͨ͠
    # Ϣχοτςετ
    UIςετ
    ਓͷͰ໨ࢹ
    !6

    View Slide

  7. % UIςετ➕ਓͷͰ໨ࢹ
    ը໘αΠζ΍ݴޠຖͷεΫγϣΛ
    1ຕͷը૾ʹग़དྷΕ͹ྑͦ͞͏
    !7

    View Slide

  8. UIςετͷৼΓฦΓ
    !8
    https://developer.apple.com/videos/play/wwdc2015/406/

    View Slide

  9. UIςετΛ͍ͯ͠Δͱͨ͘͠ͳΔࣄ
    • ಛఆͷը໘ʹ௚઀ભҠ
    • ݴޠ΍஍ҬΛมߋ
    • ϞοΫσʔλΛ౉ͯ͠ঢ়ଶมԽ
    • etc…

    View Slide

  10. https://github.com/fromkk/Lunch
    Lunch
    https://speakerdeck.com/fromkk/orecon-ios-ui-test-20171003
    !10
    2017

    View Slide

  11. LunchͷΠϯετʔϧ
    • Carthageܦ༝ͰΠϯετʔϧՄೳ
    • Cartfileʹgithub "fromkk/Lunch"Λ௥Ճ
    • carthage updateΛ࣮ߦ
    • ΞϓϦɾUIςετͦΕͧΕͷBuild Phraseʹ/usr/local/bin/carthage
    copy-frameworksΛ௥Ճ
    • ΞϓϦλʔήοτͷInput Filesʹ$(SRCROOT)/Carthage/Build/iOS/
    Lunch.frameworkΛ௥Ճ
    • UIςετλʔήοτͷInput Filesʹ$(SRCROOT)/Carthage/Build/iOS/
    LunchTest.frameworkΛ௥Ճ
    !11

    View Slide

  12. Lunchͷ࢖͍ํ(ΞϓϦଆ)
    import Lunch
    class Creator: Creatable {
    func create(_ identifier: String, userInfo: [AnyHashable: Any]?) -> T? {
    guard let name = ViewControllerName(rawValue: identifier) else { return nil }
    switch name {
    case .rootViewController:
    return RootViewController() as? T
    case .registerViewController:
    return RegisterViewController() as? T
    case .helpViewController:
    return HelpViewController() as? T
    case .contactViewController:
    return ContactViewController() as? T
    }
    }
    !12

    View Slide

  13. Lunchͷ࢖͍ํ(ΞϓϦଆ)
    import UIKit
    import Lunch
    func application(_ application: UIApplication, didFinishLaunchingWithOptions
    launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    window = UIWindow(frame: UIScreen.main.bounds)
    window?.backgroundColor = .white
    let rootViewController: UIViewController
    let creator: Creator = Creator()
    #if DEBUG
    if let viewController: UIViewController = Launcher(with: creator).launch() {
    rootViewController = viewController
    } else {
    rootViewController = creator.rootViewController()
    }
    #else
    rootViewController = creator.rootViewController()
    #endif
    window?.rootViewController = rootViewController
    window?.makeKeyAndVisible()
    }
    !13

    View Slide

  14. Lunchͷ࢖͍ํ(ΞϓϦଆ)
    import UIKit
    import Lunch
    func application(_ application: UIApplication, didFinishLaunchingWithOptions
    launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    window = UIWindow(frame: UIScreen.main.bounds)
    window?.backgroundColor = .white
    let rootViewController: UIViewController
    let creator: Creator = Creator()
    #if DEBUG
    if let viewController: UIViewController = Launcher(with: creator).launch() {
    rootViewController = viewController
    } else {
    rootViewController = creator.rootViewController()
    }
    #else
    rootViewController = creator.rootViewController()
    #endif
    window?.rootViewController = rootViewController
    window?.makeKeyAndVisible()
    }
    !14

    View Slide

  15. Lunchͷ࢖͍ํ(ΞϓϦଆ)
    import UIKit
    import Lunch
    func application(_ application: UIApplication, didFinishLaunchingWithOptions
    launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    window = UIWindow(frame: UIScreen.main.bounds)
    window?.backgroundColor = .white
    let rootViewController: UIViewController
    let creator: Creator = Creator()
    #if DEBUG
    if let viewController: UIViewController = Launcher(with: creator).launch() {
    rootViewController = viewController
    } else {
    rootViewController = creator.rootViewController()
    }
    #else
    rootViewController = creator.rootViewController()
    #endif
    window?.rootViewController = rootViewController
    window?.makeKeyAndVisible()
    }
    !15

    View Slide

  16. Lunchͷ࢖͍ํ(ςετଆ)
    import XCTest
    import LunchTest
    final class RootViewControllerTest: XCTestCase, ViewControllerTestable {
    var viewControllerName: String { return ViewControllerName.rootViewController.rawValue }
    func launch(with locale: String) -> XCUIApplication {
    let launcher = Launcher(targetViewController: self, locale: locale)
    return launcher.launch()
    }
    func testInitializeJP() {
    let _ = launch(with: "ja_JP")
    // TODO: something todo
    }
    }
    !16

    View Slide

  17. Lunchͷ࢖͍ํ(ςετଆ)
    import XCTest
    import LunchTest
    final class RootViewControllerTest: XCTestCase, ViewControllerTestable {
    var viewControllerName: String { return ViewControllerName.rootViewController.rawValue }
    func launch(with locale: String) -> XCUIApplication {
    let launcher = Launcher(targetViewController: self, locale: locale)
    return launcher.launch()
    }
    func testInitializeJP() {
    let _ = launch(with: "ja_JP")
    // TODO: something todo
    }
    }
    !17

    View Slide

  18. Lunchͷ࢖͍ํ(ςετଆ)
    import XCTest
    import LunchTest
    final class RootViewControllerTest: XCTestCase, ViewControllerTestable {
    var viewControllerName: String { return ViewControllerName.rootViewController.rawValue }
    func launch(with locale: String) -> XCUIApplication {
    let launcher = Launcher(targetViewController: self, locale: locale)
    return launcher.launch()
    }
    func testInitializeJP() {
    let _ = launch(with: "ja_JP")
    // TODO: something todo
    }
    }
    !18

    View Slide

  19. Lunchͷ࢖͍ํ(ςετଆ)
    import XCTest
    import LunchTest
    final class RootViewControllerTest: XCTestCase, ViewControllerTestable {
    var viewControllerName: String { return ViewControllerName.rootViewController.rawValue }
    func launch(with locale: String) -> XCUIApplication {
    let launcher = Launcher(targetViewController: self, locale: locale)
    return launcher.launch()
    }
    func testInitializeJP() {
    let _ = launch(with: "ja_JP")
    // TODO: something todo
    }
    }
    !19

    View Slide

  20. ͍ͩ͘͞
    https://github.com/fromkk/Lunch
    !20

    View Slide

  21. Xcode 9ΑΓXCTestʹScreenshots!
    https://developer.apple.com/videos/play/wwdc2017/409/
    !21

    View Slide

  22. εΫγϣΛࡱΔϝιουΛੜ΍͢
    import XCTest
    extension XCTestCase {
    func screenshot(_ named: String) {
    XCTContext.runActivity(named: named, block: { activity in
    let screenshot = XCUIScreen.main.screenshot()
    let attachment = XCTAttachment(screenshot: screenshot)
    attachment.lifetime = .keepAlways
    activity.add(attachment)
    })
    }
    }
    !22

    View Slide

  23. εΫγϣΛࡱΔϝιουΛੜ΍͢
    import XCTest
    extension XCTestCase {
    func screenshot(_ named: String) {
    XCTContext.runActivity(named: named, block: { activity in
    let screenshot = XCUIScreen.main.screenshot()
    let attachment = XCTAttachment(screenshot: screenshot)
    attachment.lifetime = .keepAlways
    activity.add(attachment)
    })
    }
    }
    !23

    View Slide

  24. εΫγϣΛࡱΔϝιουΛੜ΍͢
    import XCTest
    extension XCTestCase {
    func screenshot(_ named: String) {
    XCTContext.runActivity(named: named, block: { activity in
    let screenshot = XCUIScreen.main.screenshot()
    let attachment = XCTAttachment(screenshot: screenshot)
    attachment.lifetime = .keepAlways
    activity.add(attachment)
    })
    }
    }
    !24

    View Slide

  25. εΫγϣΛࡱΔϝιουΛੜ΍͢
    import XCTest
    extension XCTestCase {
    func screenshot(_ named: String) {
    XCTContext.runActivity(named: named, block: { activity in
    let screenshot = XCUIScreen.main.screenshot()
    let attachment = XCTAttachment(screenshot: screenshot)
    attachment.lifetime = .keepAlways
    activity.add(attachment)
    })
    }
    }
    !25

    View Slide

  26. ৭Μͳը໘Λ։͍ͯͻͨ͢ΒεΫγϣ
    func testInitializeJP() {
    let app = launchOtherView(with: "ja_JP")
    screenshot(#function + " screenshot")
    }
    func testInitializeUS() {
    let app = launchOtherView(with: "en_US")
    screenshot(#function + " screenshot")
    }
    .
    .
    .
    !26

    View Slide

  27. UIςετͷ࣮ߦ
    • xcodebuild build-for-testing ϏϧυͷΈ࣮ߦ
    • xcodebuild test-without-building ςετͷΈ࣮ߦ
    • Φϓγϣϯ
    • -project .xcodeprojϑΝΠϧ
    • -workspace .xcworkspaceϑΝΠϧ
    • -scheme Ϗϧυ͢ΔεΩʔϚ
    • -configuration Debug΍ReleaseͳͲ
    • -sdk iphonesimulator΍iphoneosͳͲ
    • -destination “name=iPhone X,OS=11.4,platform=iOS Simulator”
    • -only-testing:HogeTests -skip-testing:FugaTests
    !27

    View Slide

  28. ShellscriptԽ͓ͯ͘͠ͱศར
    #!/bin/sh
    xcodeproj=./Type.xcodeproj
    xcworkspace=./Type.xcworkspace
    isWorkspace=true
    test="-only-testing:TypeUITests"
    scheme="Type"
    configuration="Debug"
    sdk="iphonesimulator"
    devices=("iPhone SE" "iPhone 7" "iPhone 7 Plus" "iPhone X")
    osList=("10.3.1" "11.4")
    if $isWorkspace; then
    command="xcodebuild -workspace $xcworkspace"
    else
    command="xcodebuild -project $xcodeproj"
    fi
    rm -rf ./result
    xcrun simctl shutdown
    xcrun simctl erase all
    open -a Simulator.app
    command="$command -scheme $scheme -sdk $sdk -configuration $configuration"
    $command clean
    $command build-for-testing
    for ((i = 0; i < ${#devices[@]}; i++)); do
    device=${devices[$i]}
    for os in ${osList[@]}; do
    destination="platform=iOS Simulator,name=${device},OS=${os}"
    result=./result/"${device}"_"${os}".bundle
    echo $result
    $command test-without-building -destination "$destination" $test -resultBundlePath "$result"
    done
    done
    https://gist.github.com/fromkk/95b7fd046a081da5bfac913e1c7b31a8
    !28

    View Slide

  29. ୠ͠ɺ͜ͷ··ͩͱ஗͍
    ෳ਺୺຤ɾOSΛ௚ྻͰ࣮ߦ͍ͯ͠Δ
    !29

    View Slide

  30. !30
    https://developer.apple.com/videos/play/wwdc2018/403/

    View Slide

  31. !31

    View Slide

  32. !32

    View Slide

  33. !33

    View Slide

  34. !34

    View Slide

  35. !35
    1ճ໨ 2ճ໨ 3ճ໨ 4ճ໨ 5ճ໨ ฏۉ
    1 Simulator 145.68s 150.7s 146.76s 146.76s 153.15s 148.61s
    2 Simulators 118.4s 125.44s 115.96s 115.57s 112.9s 117.65s
    3 Simulators 124.72s 125.44s 115.09s 126.14s 111.81s 120.64s
    4 Simulators 124.53s 134.79s 132.47s 144.06s 126.19s 132.41s
    # TypeͰUIςετ૸ΒͤͯΈͨ݁Ռ

    View Slide

  36. ςετ݁ՌΛը૾ʹ·ͱΊ͍ͨ
    • xcodebuildίϚϯυʹ-resultBundlePathΦϓγϣϯΛ౉͢
    !36

    View Slide

  37. !37
    TestSummaries.plistΛύʔεͯ͠ը૾Λ͔͖ूΊΔ

    View Slide

  38. https://github.com/fromkk/TestSummaries
    • TestSummaries.plistΛύʔεͯ͠Attachmentsͷը૾ͱϚοϐϯά
    • HTMLॻ͖ग़͠ͱPNGॻ͖ग़͠ػೳ͕͋Δ
    XCTestͷUIςετͷ݁ՌΛΠΠײ͡Ͱݟ͍ͨ
    https://qiita.com/fromkk/items/38d8d4139053ae33773f
    TestSummaries
    !38
    New

    View Slide

  39. TestSummariesͷΠϯετʔϧ
    brew install fromkk/TestSummaries/testsummaries
    !39

    View Slide

  40. TestSummariesͷ࢖͍ํ
    test-summaries [--resultDirectory ] |
    [--bundlePath ] --outputPath
    --outputType [--imageScale ]
    [--backgroundColor ] [--textColor
    ]
    !40

    View Slide

  41. TestSummariesͷ࣮ߦ
    # ςετͷ࣮ߦ
    $ xcodebuild test -workspace Type.xcworkspace -scheme Type -
    configuration Debug -sdk iphonesimulator -destination
    “OS=11.4,name=iPhone X,platform=iOS Simulator" -resultBundlePath ./
    uitests/iphonex.bundle
    .
    .
    .
    # ݁ՌΛ·ͱΊΔ
    $ test-summaries --resultDirectory ./uitests --outputPath ./
    uitests/result.png —outputType PNG --imageScale 2
    !41

    View Slide

  42. TestSummariesͷ࣮ߦ
    # ςετͷ࣮ߦ
    $ xcodebuild test -workspace Type.xcworkspace -scheme Type -
    configuration Debug -sdk iphonesimulator -destination
    “OS=11.4,name=iPhone X,platform=iOS Simulator" -resultBundlePath ./
    uitests/iphonex.bundle
    .
    .
    .
    # ݁ՌΛ·ͱΊΔ
    $ test-summaries --resultDirectory ./uitests --outputPath ./
    uitests/result.png —outputType PNG --imageScale 2
    !42

    View Slide

  43. ݁Ռ
    !43
    ݁Ռ

    View Slide

  44. !44

    View Slide

  45. ͍ͩ͘͞
    https://github.com/fromkk/TestSummaries
    !45

    View Slide

  46. Demo
    !46

    View Slide

  47. ͜͜·ͰͷৼΓฦΓ
    • XCTAssertແ͠ͰUIςετΛ࣮ߦ
    • εΫϦʔϯγϣοτΛࡱӨ
    • εΫϦʔϯγϣοτը૾Λ1ຕʹ·ͱΊΔ
    !47

    View Slide

  48. ͜ͷঢ়ଶ͔ΒͳΒXCTAssertΛ
    গͣͭ͠௥Ճग़དྷͦ͏͡Όͳ͍ʁ
    !48

    View Slide

  49. ྫ͑͹
    func testHoge() {
    let app = launchOtherView(with: "ja_JP")
    screenshot(#function + " screenshot")
    }
    !49

    View Slide

  50. ྫ͑͹
    func testHoge() {
    let app = launchOtherView(with: “ja_JP")
    let hogeLagel = app.staticTexts["hogeLagel"]
    XCTAssertTrue(hogeLagel.exists)
    XCTAssertEqual(hogeLagel.label, "hoge")
    screenshot(#function + " screenshot")
    }
    !50

    View Slide

  51. ྫ͑͹
    func testHoge() {
    let launcher = Launcher(targetViewController: self)
    let app = launcher.launch()
    XCTAssertTrue(app.staticTexts[“hogeLabel"].exists)
    XCTAssertEqual(app.staticTexts["hogeLabel"].label, "hoge")
    screenshot(#function)
    }
    !51

    View Slide

  52. ྫ͑͹
    func testHoge() {
    let launcher = Launcher(targetViewController: self,
    userInfo: [
    "MOCK_JSON": "{\"hoge\": \"fuga\"}"
    ])
    let app = launcher.launch()
    XCTAssertTrue(app.staticTexts["hogeLabel"].exists)
    XCTAssertEqual(app.staticTexts["hogeLabel"].label, "fuga")
    screenshot(#function)
    }
    !52

    View Slide

  53. ·ͱΊ
    • UIςετΛςετͱͯ͠ͷ༻్Ͱ͸ͳࣗ͘ಈεΫγϣࡱӨػೳͱͯ͠
    ར༻ͯ͠Έͨ
    • ෳ਺ͷεΫϦʔϯγϣοτΛ̍ຕͷը૾ʹ·ͱΊΔࣄͰɺσβΠϯ่
    ΕΛ໨ࢹͰ֬ೝग़དྷΔ༷ʹͯ͠Έ·ͨ͠
    • ੜ੒͞ΕΔͷ͕ը૾ͳͷͰνʔϜ΁ͷڞ༗΋؆୯*
    • ඞཁͳը໘͕શͯىಈग़དྷΔ༷ʹͳͬͨΒςετ΋ॻ͚Δؾ͕͠·ͤ
    Μ͔ʁ
    !53

    View Slide

  54. ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠+
    !54

    View Slide