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

249b3122eee454c0a818bfe7851418e4?s=47 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/ どしどしご応募ください!

249b3122eee454c0a818bfe7851418e4?s=128

fromkk

August 30, 2018
Tweet

Transcript

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

  2. Profile struct Profile { let name = "Kazuya Ueoka" let

    twitter = "@fromkk" let github = "fromkk" let qiita = "fromkk" let company = "Timers Inc." } • !2
  3. ͜Μͳࣄແ͍Ͱ͔͢ʁ !3

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

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

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

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

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

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

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

  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
  12. Lunchͷ࢖͍ํ(ΞϓϦଆ) import Lunch class Creator: Creatable { func create<T>(_ 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
  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
  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
  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
  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
  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
  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
  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
  20. ͍ͩ͘͞ https://github.com/fromkk/Lunch !20

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

  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
  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
  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
  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
  26. ৭Μͳը໘Λ։͍ͯͻͨ͢ΒεΫγϣ func testInitializeJP() { let app = launchOtherView(with: "ja_JP") screenshot(#function

    + " screenshot") } func testInitializeUS() { let app = launchOtherView(with: "en_US") screenshot(#function + " screenshot") } . . . !26
  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
  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
  29. ୠ͠ɺ͜ͷ··ͩͱ஗͍ ෳ਺୺຤ɾOSΛ௚ྻͰ࣮ߦ͍ͯ͠Δ !29

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

  31. !31

  32. !32

  33. !33

  34. !34

  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ςετ૸ΒͤͯΈͨ݁Ռ
  36. ςετ݁ՌΛը૾ʹ·ͱΊ͍ͨ • xcodebuildίϚϯυʹ-resultBundlePathΦϓγϣϯΛ౉͢ !36

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

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

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

  40. TestSummariesͷ࢖͍ํ test-summaries [--resultDirectory <resultDirectory>] | [--bundlePath <bundlePath>] --outputPath <outputPath> --outputType

    <outputType> [--imageScale <imageScale>] [--backgroundColor <backgroundColor>] [--textColor <textColor>] !40
  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
  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
  43. ݁Ռ !43 ݁Ռ

  44. !44

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

  46. Demo !46

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

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

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

    + " screenshot") } !49
  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
  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
  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
  53. ·ͱΊ • UIςετΛςετͱͯ͠ͷ༻్Ͱ͸ͳࣗ͘ಈεΫγϣࡱӨػೳͱͯ͠ ར༻ͯ͠Έͨ • ෳ਺ͷεΫϦʔϯγϣοτΛ̍ຕͷը૾ʹ·ͱΊΔࣄͰɺσβΠϯ่ ΕΛ໨ࢹͰ֬ೝग़དྷΔ༷ʹͯ͠Έ·ͨ͠ • ੜ੒͞ΕΔͷ͕ը૾ͳͷͰνʔϜ΁ͷڞ༗΋؆୯* •

    ඞཁͳը໘͕શͯىಈग़དྷΔ༷ʹͳͬͨΒςετ΋ॻ͚Δؾ͕͠·ͤ Μ͔ʁ !53
  54. ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠+ !54